Next.js Validation

Minboy·2024년 3월 4일
1
post-thumbnail

Chapter 14

Form validation

Client-Side validation

클라이언트 단에서 form의 유효성을 검증하는 방법은 여러가지가 있다. 가장 간단한 방법은 <input><select> 태그에 required 속성을 추가하는 것이다.

<input
  id="amount"
  name="amount"
  type="number"
  placeholder="Enter USD amount"
  className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
  required
/>

이렇게하면 빈 input을 제출하려고 할 때 브라우저에서 오류를 보여준다.

Server-Side validation

서버에서 form을 검증함으로써 다음과 같은 효과를 얻을 수 있다.

  • 데이터를 DB로 보내기전에 형식을 확실하게 보장할 수 있다.
  • 클라이언트 단의 검증을 우회하는 악의적인 사용자들의 위험을 줄일 수 있다.
  • 유효한 데이터에 대한 하나의 정확성을 가질 수 있다.
'use client';

// ...
import { useFormState } from 'react-dom';

useFormState 는 훅이기 때문에 'use client' 지시문으로 클라이언트 컴포넌트임을 명시해주어야한다.

Form 컴포넌트 내부에서, useFormState 훅은

  • 두 개의 인자를 받는다. : (action, initialState)
  • 두개의 값을 리턴한다. : [state, dispatch] - form state 와 dispatch 함수이다. (React의 useReducer 훅과 유사하다.)

기존의 action을 useFormState 의 인자로 넘겨주고, form의 action 속성에는 dispatch 함수를 전달해주자.

// ...
import { useFormState } from 'react-dom';

export default function Form({ customers }: { customers: CustomerField[] }) {
  const initialState = { message: null, errors: {} };
  const [state, dispatch] = useFormState(createInvoice, initialState);

  return <form action={dispatch}>...</form>;
}

initialState 는 어떤 것도 될 수 있다.

Server Action쪽 코드에 작성되어 있던 FormSchema 를 수정해보자.

const FormSchema = z.object({
  id: z.string(),
  customerId: z.string({
    invalid_type_error: 'Please select a customer.',
  }),
  amount: z.coerce
    .number()
    .gt(0, { message: 'Please enter an amount greater than $0.' }),
  status: z.enum(['pending', 'paid'], {
    invalid_type_error: 'Please select an invoice status.',
  }),
  date: z.string(),
});

이제 Zod는 각 타입과 맞는지 검사해 맞지 않는다면 우리가 설정한 메세지를 리턴해줄 것이다. 이 메세지를 활용해 사용자에게 잘못되었음을 알려주자.

// This is temporary until @types/react-dom is updated
export type State = {
  errors?: {
    customerId?: string[];
    amount?: string[];
    status?: string[];
  };
  message?: string | null;
};

export async function createInvoice(prevState: State, formData: FormData) {
  // ...
}

먼저 createInvoice 함수도 수정을 해주어야한다.

  • formData - 이전과 동일
  • prevState - useFormState 훅으로부터 전달된 state이다.

Zod의 parse() 부분도 safeParse() 로 변경해주어야한다.

export async function createInvoice(prevState: State, formData: FormData) {
  // Validate form fields using Zod
  const validatedFields = CreateInvoice.safeParse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });

  // If form validation fails, return errors early. Otherwise, continue.
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Missing Fields. Failed to Create Invoice.',
    };
  }

  // ...
}

parse()는 검증이 실패했을 때 오류를 throw 하지만, safeParse()는 성공 또는 실패를 포함한 객체를 리턴해준다. 이를 활용하여 오류 발생 시 유저에게 UI를 통해 잘못되었음을 알려줄 수 있다.

이제 다시 form으로 돌아와서 state 를 통해 오류를 감지하고 사용자에게 알려주자.

<form action={dispatch}>
  <div className="rounded-md bg-gray-50 p-4 md:p-6">
    {/* Customer Name */}
    <div className="mb-4">
      <label htmlFor="customer" className="mb-2 block text-sm font-medium">
        Choose customer
      </label>
      <div className="relative">
        <select
          id="customer"
          name="customerId"
          className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
          defaultValue=""
          aria-describedby="customer-error"
        >
          <option value="" disabled>
            Select a customer
          </option>
          {customerNames.map((name) => (
            <option key={name.id} value={name.id}>
              {name.name}
            </option>
          ))}
        </select>
        <UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
      </div>
      <div id="customer-error" aria-live="polite" aria-atomic="true">
        {state.errors?.customerId &&
          state.errors.customerId.map((error: string) => (
            <p className="mt-2 text-sm text-red-500" key={error}>
              {error}
            </p>
          ))}
      </div>
    </div>
    // ...
  </div>
</form>

state와 삼항 연산자를 활용해 사용자에게 오류를 보여주는 것을 확인할 수 있다.

profile
🐧

0개의 댓글

관련 채용 정보