라이브러리/React Hook Form

[React Hook Form] Yup으로 마무리하는 유효성 검사

눙엉 2024. 2. 20. 23:55

React Hook Form을 사용하여 폼을 만들 때, 마지막 단계로 폼의 유효성 검사를 하는 것은 중요한 과정 중 하나입니다. 이때 유효성 검사란 사용자가 입력한 데이터가 원하는 규칙에 부합하는지 확인하는 작업을 의미합니다. React Hook Form에서는 register 메서드를 사용하여 각 입력 필드에 대한 유효성 검사 규칙을 정의할 수 있습니다. 그러나 폼이 복잡해지고 유효성 검사 규칙이 많아질수록 JSX 코드는 복잡해지고 가독성이 떨어질 수 있습니다.

 

이런 상황에서 Yup을 사용하면 유효성 검사 규칙을 한 곳에서 효과적으로 관리할 수 있습니다. Yup은 선언적으로 유효성 검사 규칙을 정의할 수 있어 코드의 일관성을 유지하고 가독성을 향상합니다. 따라서 JSX(TSX) 코드에서는 각 입력 필드에 대한 복잡한 유효성 검사 규칙이 아니라, 간단한 register 호출만으로 깔끔하게 표현할 수 있습니다.

 

먼저, Yup을 사용하지 않고 유효성 검사를 진행하는 코드를 확인해 보겠습니다.

<form onSubmit={handleSubmit(() => console.log("submit"))}>
  <label htmlFor="name">이름</label>
  <input
    id="name"
    {...register("name", {
      required: "필수 입력입니다.",
      minLength: { value: 2, message: "최소 2글자 이상 작성해주세요." },
      maxLength: { value: 5, message: "최대 5글자 이하로 작성해주세요." },
    })}
  />
  {errors.name && <p>{errors.name.message}</p>}
  <label htmlFor="age">나이</label>
  <input
    type="number"
    id="age"
    {...register("age", {
      min: { value: 1, message: "1 이상의 숫자만 입력 가능합니다." },
      max: { value: 100, message: "100 이하의 숫자만 입력 가능합니다." },
    })}
  />
  {errors.age && <p>{errors.age.message}</p>}
  <label htmlFor="phone">휴대폰 번호</label>
  <input
    id="phone"
    {...register("phone", {
      required: "필수 입력입니다.",
      pattern: {
        value: /\d{2,3}-\d{3,4}-\d{4}/g,
        message: "전화번호 패턴이 아닙니다.",
      },
    })}
  />
  {errors.phone && <p>{errors.phone.message}</p>}
  <label htmlFor="fruit">좋아하는 과일</label>
  <select
    id="fruit"
    {...register("fruit", {
      required: "필수 입력입니다.",
    })}
  >
    <option>사과</option>
    <option>바나나</option>
    <option>포도</option>
    <option>복숭아</option>
    <option>기타</option>
  </select>
  {watch("fruit") === "기타" && (
    <>
      <label htmlFor="fruitEtc">좋아하는 과일</label>
      <input
        id="fruitEtc"
        {...register("fruitEtc", {
          required: watch("fruit") === "기타" ? "필수 입력입니다." : false,
        })}
      />
    </>
  )}
  {errors.fruitEtc ? (
    <p>{errors.fruitEtc.message}</p>
  ) : (
    errors.fruit && <p>{errors.fruit.message}</p>
  )}
  <button>전송</button>
</form>;

폼을 입력받는 input 요소와 input을 연결해 주는 label 요소, 에러 메시지를 보여주는 p 태그까지 많지 않은 폼 입력 요소들이지만 JSX(TSX)는 많은 코드들로 파악하기 어렵게 되었습니다.

 

다음으로, Yup을 사용해 유효성 검사를 진행하는 코드를 확인해 보겠습니다.

const schema = yup
  .object()
  .shape({
    name: yup
      .string()
      .required("필수 입력입니다.")
      .min(2, "최소 2글자 이상 작성해주세요.")
      .max(5, "최대 5글자 이하로 작성해주세요."),
    age: yup
      .number()
      .min(1, "1 이상의 숫자만 입력 가능합니다.")
      .max(100, "100 이하의 숫자만 입력 가능합니다."),
    phone: yup
      .string()
      .required("필수 입력입니다.")
      .matches(/\d{2,3}-\d{3,4}-\d{4}/g, "전화번호 패턴이 아닙니다."),
    fruit: yup.string().required("필수 입력니다."),
    fruitEtc: yup
      .string()
      .required()
      .when("fruit", {
        is: (value: string) => value === "기타",
        then: () => yup.string().required("필수 입력입니다."),
        otherwise: () => yup.string(),
      }),
  })
  .required();



<form onSubmit={handleSubmit(() => console.log("submit"))}>
  <label htmlFor="name">이름</label>
  <input id="name" {...register("name")} />
  {errors.name && <p>{errors.name.message}</p>}
  <label htmlFor="age">나이</label>
  <input type="number" id="age" {...register("age")} />
  {errors.age && <p>{errors.age.message}</p>}
  <label htmlFor="phone">휴대폰 번호</label>
  <input id="phone" {...register("phone")} />
  {errors.phone && <p>{errors.phone.message}</p>}
  <label htmlFor="fruit">좋아하는 과일</label>
  <select id="fruit" {...register("fruit")}>
    <option>사과</option>
    <option>바나나</option>
    <option>포도</option>
    <option>복숭아</option>
    <option>기타</option>
  </select>
  {watch("fruit") === "기타" && (
    <>
      <label htmlFor="fruitEtc">좋아하는 과일</label>
      <input id="fruitEtc" {...register("fruitEtc")} />
    </>
  )}
  {errors.fruitEtc ? (
    <p>{errors.fruitEtc.message}</p>
  ) : (
    errors.fruit && <p>{errors.fruit.message}</p>
  )}
  <button>전송</button>
</form>;

Yup을 사용하여 유효성 검사 로직과 렌더링 로직을 분리하는 것은 코드의 가독성과 유지보수성을 향상하는 좋은 방법입니다. 각 부분이 독립적으로 존재하면서도 필요에 따라 쉽게 수정할 수 있도록 구성됩니다.

 

이러한 접근은 코드를 이해하기 쉽게 만들어주며, 향후 유효성 검사 규칙이 변경되거나 추가될 경우 해당 부분만 수정하면 되므로 유지보수가 훨씬 편리해집니다.

 

위에서 작성했던 Yup 코드에 대해 설명하겠습니다. 아래의 코드는 Yup 코드만 따로 분리해 작성했습니다.

const schema = yup
  .object()
  .shape({
    name: yup
      .string()
      .required("필수 입력입니다.")
      .min(2, "최소 2글자 이상 작성해주세요.")
      .max(5, "최대 5글자 이하로 작성해주세요."),
    age: yup
      .number()
      .required("필수 입력입니다.")
      .min(1, "1 이상의 숫자만 입력 가능합니다.")
      .max(100, "100 이하의 숫자만 입력 가능합니다."),
    phone: yup
      .string()
      .required("필수 입력입니다.")
      .matches(/\d{2,3}-\d{3,4}-\d{4}/g, "전화번호 패턴이 아닙니다."),
    fruit: yup.string().required("필수 입력니다."),
    fruitEtc: yup
      .string()
      .required()
      .when("fruit", {
        is: (value: string) => value === "기타",
        then: () => yup.string().required("필수 입력입니다."),
        otherwise: () => yup.string(),
      }),
  })
  .required();

yup.object(). shape()의 인수에 전달하는 객체로 유효성 스키마를 정의하고 있습니다. 객체에는 name, age, phone, fruit, fruitEtc 필드가 존재하고 age를 제외한 모든 필드가 문자열 타입입니다.

 

name의 코드를 확인해 봅시다. yup.string(). required("필수입력입니다."). min(2, "최소 2글자 이상 작성해 주세요."). max(5, "최대 5글자 이하로 작성해 주세요.") 메서드 체이닝을 이용해 각 필드의 유효성 검사를 추가할 수 있습니다. 제일 처음 데이터 타입을 설정합니다. 그리고 값이 필수 값인지 설정하고 데이터에 필요한 유효성 로직을 메서드 체이닝을 이용해 추가하면 됩니다. 메서드 체이닝을 이용해 유효성 검사를 진행할 때 순서에 따라 결과가 달라질 수 있으니 주의해야 합니다. 필수값 체크를 먼저 하는 것이 일반적으로 더 좋은 방법으로 알고 있습니다. 예를 들어 필수값 체크를 먼저 하면 해당 필드가 존재하지 않으면 다른 유효성 검사를 수행하지 않고 즉시 유효성 검사를 실패시킬 수 있기 때문에 불필요한 검사를 줄여주고, 코드의 효율성도 높일 수 있습니다.

 

string, number, boolean 타입은 Yup의 공식문서를 참고해서 직관적으로 사용하기 쉽지만 object, array 타입은 처음 접하는 경우 메서드 체이닝 순서에 유효성 검사하는 방법이 달라질 수 있어 어떻게 달라지는지 아래 코드와 함께 설명하겠습니다.

const schemaA = yup.object().shape({
  test: yup.object().shape({ a: yup.string() }).required(),
});

const schemaB = yup.object().shape({
  test: yup.object().shape({ a: yup.string().required() }),
});

schemaAschemaB는 유효성 검사가 동일할 것 같으나 다른 동작을 하게 됩니다. 하나씩 차근차근 확인해 보겠습니다.

 

두 개의 스키마를 보기 쉽게 코드로 작성해 보겠습니다. schemaA, schemaB는 모두 객체이며 객체 내부의 test도 객체를 가지고 있는 것을 확인할 수 있습니다. 하지만 필수로 체크하는 값이 다릅니다. schemaA의 경우 test가 객체를 필수로 가져야 하며, schemaB의 경우 test의 객체 내부에 a라는 필드와 값을 필수로 가져야 합니다.

// test 객체 내부에 a 필드가 필수로 존재해야 됩니다.
const schemaA = {
  test: {
    a: "";
  };
};

// test 객체가 필수로 존재해야 됩니다.
const schemaB = {
  test: {
    a: "";
  };
};

 

 

이번에는 배열의 예시를 아래의 코드와 함께 설명하겠습니다.

const schemaA = yup.array().of({ a: yup.string().required() });

const schemaB = yup.array().of({ a: yup.string() }).required();

schemaAschemaB는 매우 비슷한 형태로 생겼습니다. 하지만 위의 object 타입처럼 비슷하나 유효성 검사의 결과는 매우 다르다는 것을 알 수 있습니다.

 

두 개의 스키마를 보기 쉽게 코드로 작성해 보겠습니다. schemaA, schemaB는 모두 배열이며 배열 내부에 객체를 포함하고 있습니다. 여기서도 마찬가지로 필수로 체크하는 것이 다릅니다. schemaA의 경우 배열 내부의 객체가 a를 필수로 존재해야 하며, schemaB의 경우 배열 그 자체를 필수로 존재해야 합니다.

// 배열 내부의 객체안에 a가 필수로 존재해야 합니다.
const schemaA = [{a: ""}];

// 배열 자체가 필수로 존재해야 합니다.
const schemaB = [{a: ""}];

 

 


React Hook Form을 사용할 때 유효성 검사를 위해 Yup과 함께 사용하는 방법을 알아보았습니다. 유효성 검사하는 코드를 한 곳으로 모아 관리할 수 있어 코드의 가독성을 높일 수 있는 방법이라 좋은 것 같습니다. 다만 폼의 내용이 너무 많아지면 Yup의 코드도 길어져 유효성 검사 하는 코드를 한 번에 파악하지 못하는 단점이 존재 합니다. 그리고 처음 Yup을 접할 때 한 가지의 방법이 아닌 여러 가지의 방법으로 유효성 검사를 통과시켜 주니 어떤 것이 정답인지 많이 헷갈리기도 했습니다. 10을 구하는 방법으로 비유하자면 무조건 5 + 5 만 가능한 것이 아니라 1을 10번 더하거나 2 * 5를 한다던지 이렇게 여러 방법을 통해서 10을 구하면 되는 듯한 느낌을 받았습니다. 제가 작성한 사용법을 보고 여러분들도 헷갈리지 않게 사용할 수 있었으면 좋겠습니다.