logo
새로운 블로그로 이전하였습니다.

zod와 react hook form으로 사용자의 입력값 유효성 검사하기

  • zod
  • react-hook-form
  • validation

들어가며

GGF 프로젝트에서는 react hook formzod를 함께 사용해 유저의 입력값 유효성 검사를 진행했었습니다.

react hook formzod가 무엇이고, 이것들을 구체적으로 어떻게 사용했으며, 그 과정에서 느낀 점들은 무엇인지를 기록해보려 합니다.


❔ react hook form

react hook form은 사용하기 쉬운 유효성 검사를 통해 성능이 우수하고 유연하며 확장 가능한 양식을 제공하는 라이브러리로, 비제어 컴포넌트 방식으로 구현되어있다는 것이 특징입니다.

비제어 컴포넌트 Uncontrolled Components

비제어 컴포넌트제어 컴포넌트(Controlled Component)에서 state를 사용해 form의 값을 가져오는 것과 다르게, ref를 사용해서 DOM에서 직접 form 값을 가져옵니다. 사용자가 formsubmit하기 전까지는 리렌더링을 발생시키지 않고 값을 동기화하지 않습니다.

이 덕분에 불필요한 리렌더링이 발생하지 않습니다.

장점

1. `formProvider`를 사용해 `props drilling` 없이 `form`의 상태 공유가 가능하다.
2. native form validation을 제공한다.
3. 용량이 작고 dependency가 없다.
4. form validation을 위한 표준 HTML을 기반으로 만들어졌다.
5. Yub, Zod 혹은 커스텀 validation과 함께 쓸 수 있다.
6. devTools를 제공한다.

예시

로그인 페이지를 만든다고 가정해보겠습니다.

ggf login page 출처: GGF 로그인 페이지

UI는 위처럼 아이디와 비밀번호를 입력받는 input과 이 input들이 포함된 form을 제출하기 위한 로그인 버튼이 필요할 것입니다.

어떤 기능이 필요할까요? 상황에 따라 달라지는 부분이 있겠지만, 기본적으로는 세 가지 기능이 떠오릅니다.

1. 값 입력받기 -> 사용자로부터 아이디와 비밀번호 값을 `input`으로 입력받아야 합니다.
2. 유효성 검사 -> 입력받은 값이 유효한지 검사해야 합니다.
3. 유효성 검사 피드백 전달 -> 유효성 검사의 피드백을 UI로 전달해야합니다.

react hook form을 사용하면 이 모든 기능들을 비교적 쉽게 구현할 수 있습니다.

에러메시지

import { ErrorMessage } from '@hookform/error-message';
 
// ...
 
const { formState: { errors } } = useFormContext();
 
// ...
<span className={cx('input-field-err-msg')}>
  <ErrorMessage errors={errors} name={name} render={({ message }) => <p>{message}</p>} />
</span>

react hook form 자체의 ErrorMessage 컴포넌트를 사용해서 에러 메시지를 쉽게 UI에 보여줄 수 있습니다.

자체 validation을 사용한 유효성 검사

위에서 말했듯이 react hook form 자체에서 유효성 검사가 가능합니다.

register에 두 번째 인자로 전달하는 객체에 validation 옵션을 사용하면 됩니다. 필수 입력값 여부를 결정하는 required, 입력값의 최대/최소 길이를 정하는 maxLength/minLength, 원하는 정규식으로 입력값을 검사하는 pattern 외에도 다양한 옵션들이 있습니다.

register validation 옵션들 알아보기

GGF 프로젝트 전의 프로젝트에서 사용했던 register validation 옵션들은 다음과 같습니다.

<input
  {...register(name, {
    required: isRequired ? 'This is Required.' : errorMessage?.empty,
    pattern: {
      value: regex, // constansts에 저장해둔 regex pattern
      message: errorMessage?.invalid, // constansts에 저장해둔 에러메시지 문자열
    },
    minLength: name === 'password' && { // input의 name이 password면(= 비밀번호 입력 input이면)
        value: 8, // 8자 제한
        message: 'Please write at least 8 characters', // validation 검사 실패시 에러메시지
      },
  })}
  {...props}
/>

자체 validation의 단점

여기까지 보셨을 때 react hook form으로도 충분히 validation이 가능하다는 것을 느끼실 수 있을 것입니다.

TickyTokcy 프로젝트에서 사용해본 결과, 다음과 같은 아쉬움들을 느꼈습니다.

1. <input>의 코드가 매우 길어진다. -> 코드 가독성이 떨어진다. 
2. validation 로직과 input의 UI가 섞인다. -> 코드 가독성과 유지보수성이 떨어진다.
3. 복잡한 검증 로직은 작성하기 어렵다. -> validate를 사용할 수 있긴하지만, 복잡한 로직까지는 작성이 어렵다고 느꼈다.

평소에 리액트에서 컴포넌트를 만들 때 뷰와 로직을 분리하는 것을 우선으로 했기에, 특히 2번이 매우 아쉬운 부분이었습니다.

후에 진행한 GGF프로젝트에서는 이를 개선해보고 싶었고, 방법을 찾던 도중 zod를 알게 되었습니다.


❔ zod

zod는 TypeScript-first 스키마 선언 및 유효성 검사 라이브러리입니다.

개발자 친화적으로 설계되었으며, 유효성 검사기를 한 번만 선언하면 자동으로 정적 타입스크립트 타입을 추론한다는 특징이 있습니다.

장점

1. Zero dependencies
2. Works in Node.js and all modern browsers -> Node.js를 비롯한 모든 모던 브라우저에서 작동한다.
3. Tiny: 8kb minified + zipped -> 8kb라는 작은 용량!
4. Immutable: methods (e.g. .optional()) return a new instance -> 메서드를 호출할 때마다 원본 객체를 변경하지 않고 새로운 객체를 반환하여 불변성을 지킨다.
5. Concise, chainable interface -> 메서드 체이닝을 통해 여러 메서드를 연결해서 사용할 수 있다.
6. Functional approach: parse, don't validate -> 데이터를 검증(validate)하기보다는 파싱(parse)하는 함수형 프로그래밍 접근 방식을 따른다.
7. Works with plain JavaScript too! You don't need to use TypeScript.

zod는 위와 같이 많은 장점이 있지만, zod를 바로 도입했던 이유는 react hook form과의 시너지때문이었습니다.

예시

위에서 react hook form의 자체 validation으로만 만들었던 로그인 폼을 react hook formzod를 함께 사용해서 validation 해보겠습니다.

먼저 zod에서는 값의 유효성 검사를 위해선 스키마라는 것을 만들어야 합니다. 아래처럼 간단히 만들 수 있습니다.

  • 스키마는 데이터 구조를 정의하고 검증하는데 사용되는 일종의 설계도로, 여기에서는 form으로 들어올 값들의 타입규칙을 정해두는 것이라고 간단하게 생각하시면 됩니다.
const SigninSchema = z.object({
  email: z
    .string() // 문자열
    .min(1, { message: ERROR_MESSAGE.email.min }) // 최소 1자 이상
    .email({message: ERROR_MESSAGE.email.regex, // email regex 
  }),
  password: z
    .string() // 문자열
    .min(8, { message: ERROR_MESSAGE.password.min }) // 최소 8자 이상
    .regex(REGEX.password, ERROR_MESSAGE.password.regex), // password regex
});

다음엔 만든 스키마를 react hook formresolver로 연결시켜주어야 합니다.

  const methods = useForm({
    mode: 'all',
    resolver: zodResolver(SigninSchema),
  });

후에는 formonSubmit에 위에서 만든 submit을 걸어주면 됩니다. (로그인 폼의 자세한 코드는 여기에서 확인해주세요)

폼의 UI는 아래와 같습니다.

<FormProvider {...methods}>
  <form className={cx('signin-form')} onSubmit={methods.handleSubmit(handleSigninSubmit)}>
    <fieldset className={cx('fieldset')}>
      <legend>회원가입 정보 등록</legend>
      <AuthInputField label='Email' name='email' type='email' placeholder='Type your email' />
      <AuthInputField
        label='Password'
        name='password'
        type='password'
        maxLength={15}
        placeholder='Type your password'
      />
      <BaseButton type='submit' theme='fill' size='large' isDisabled={!isValid} isQuantico>
        Match Now
      </BaseButton>
    </fieldset>
  </form>
</FormProvider>

react hook form과 zod를 같이 써야하는 이유

위에서 예시 코드로 보셨을 때, 전자와 후자의 차이가 느껴지시나요? react hook form + zod를 사용해보고 나서 이 둘을 같이 써야하는 이유를 아래와 같이 정리해보았습니다.

1. 타입 안전성을 보장할 수 있다.
2. 스키마를 활용해 보다 강력하게 데이터 검증이 가능하다.
3. 복잡한 데이터 구조나 검증 로직을 정의할 수 있다.
4. 스키마를 다양한 곳에서 재사용할 수 있다.
5. validation 로직과 view를 분리할 수 있다. -> 코드 가독성, 유지 보수성 향상
6. validation의 구체적인 내용을 직관적으로 파악할 수 있다. 

마무리하며

react hook form과 zod 둘 다 접해본 경험이 많지 않아 초기에 사용하는데 많은 시행착오를 겪었습니다(자세한 내용은 차차 포스팅하겠습니다.).

하지만 익숙해지고나니 form validaiton에 이렇게 좋은 도구가 없다는 생각이 들더군요! react hook form도 공식 docs가 매우 친절한 편이고, devTools도 제공하여 너무 좋았습니다.

zod는 제공하는 기능 중 10%도 써보지 못한 것 같다는 생각이 들었습니다. 다음에는 zod의 여러 기능들을 제대로 활용하여 더 강력한 타입검증을 해보고 싶습니다.

글에서 보여준 예시 코드 전문은 전부 GGFTickyTokcy 깃헙에서 확인해보실 수 있습니다.

제가 포스팅한 내용 중 잘못된 부분이 있다면 댓글로 언제든 남겨주시면 정정하도록 하겠습니다!


Sources