react

zod를 이용해 회원가입 폼을 만들어보기

2023. 6. 18. 13:40
목차
  1. zod는
  2. 작성해보기
  3. React Hook Form에서 사용해보자
  4. 마치며

zod는

타입스크립트를 우선하는 스키마 선언 / 검증 라이브러리입니다.

타입스크립트의 타입체킹을 보완해주는 역할을 수행한다고도 볼 수 있는데

쉽게 생각하면 폼,인풋에 필연적으로 따라오는 유효성검증 로직을 해결해주는 라이브러리입니다.

 

기본적인 개념과 사용방법은 아래 링크에서 참고해주시기 바랍니다.

https://xionwcfm.tistory.com/346

 

zod 라이브러리 사용법 간단히 익히기

zod zod는 타입스크립트를 우선하는 스키마 선언 / 검증 라이브러리입니다. validation을 도와준다고도 생각할 수 있는데 문법은 간결하고 쉬우면서도 기능은 강력합니다. 공식 documentation에 따르면

xionwcfm.tistory.com

 

이번엔 위 링크에서 얻은 선수지식을 기반으로

사용할 수 있는 회원가입 로직을 작성해보겠습니다.

회원가입 로직에서 요구되는 사항은 다음과 같습니다.

 

1. Phone number 유효성 검증이 가능할 것

2. 패스워드 확인 인풋이 패스워드 인풋에 작성된 내용과 같은지 비교할 수 있을 것

3. email을 양식에 맞게 입력받을 수 있을 것

 

이정도의 요구사항을 두고 스키마를 작성해보겠습니다.


작성해보기

npm i zod react-hook-form @hookform/resolvers

우선 필요한 종속성을 설치하겠습니다.

zod와 react-hook-form 그리고 그 둘을 통합하는 것을 도와줄 resolver를 설치합니다.

const phoneRegex = new RegExp(/^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/);

핸드폰 넘버의 유효성을 검증할 정규식도 작성해주겠습니다.

01로시작하면서 3번째에는 0/1/6/7/8/9만 들어올 수 있고

중간글자는 숫자이면서 길이가 3~4

마지막은 숫자이면서 길이가 4여야한다는 로직입니다.

중간의 -바는 옵셔널로 들어가도되고 안들어가도 됩니다.

옵셔널을 지워주고 싶다면 물음표를 지워주면 됩니다.

const phoneRegex = new RegExp(/^01([0|1|6|7|8|9])-([0-9]{3,4})-([0-9]{4})$/);

이런식으로요!

지금 작성해둔 정규식은 나중에 유용하게 사용할 것입니다.

import z from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const phoneRegex = new RegExp(/^01([0|1|6|7|8|9])-([0-9]{3,4})-([0-9]{4})$/);

const LoginSchema = z
  .object({
    email: z
      .string()
      .min(1, { message: 'This field has to be filled' })
      .email({ message: 'Invalid email address' }),
    name: z.string().min(2).max(10),
    password: z.string().min(8),
    checkPassword: z.string().min(8),
    phone: z.string().regex(phoneRegex, 'Invalid PhoneNumber'),
  })
  .superRefine(({ checkPassword, password }, ctx) => {
    if (checkPassword !== password) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['checkPassword'],
      });
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['password'],
      });
    }
  });

type LoginType = z.infer<typeof LoginSchema>;

다음은 zod를 이용하여 스키마를 정의해주겠습니다.

.min(1, {message:""})의 형식으로 스키마의 형식에 어긋나는 입력이 들어왔을 때

표시할 메시지를 message 키의 밸류에 표시해줄 수 있습니다.

만약 message를 따로 정의해주지 않는 경우에는 zod에서 기본으로 제공해주는

에러메시지를 사용하게 됩니다.

phone: z.string().regex(phoneRegex, 'Invalid PhoneNumber')

z.string() 에는 regex()를 체이닝할 수 있습니다.

메서드 이름에서 예상할 수 있듯이 regex() 메서드에는

정규식을 전달해줄 수 있습니다.

아까 작성해둔 정규식을 전달해주겠습니다.

.superRefine(({ checkPassword, password }, ctx) => {
    if (checkPassword !== password) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['checkPassword'],
      });
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['password'],
      });
    }
  });

zod가 제공해주는 refine , superRefine 메서드가 존재합니다.

이는 좀 더 까다로운 유효성검증을 도와주는 메서드라고 생각할 수도 있습니다.

superRefine메서드의 첫번째인자에는 콜백함수가 전달됩니다.

이 콜백함수의 첫번째 매개변수에는 우리가 정의한 스키마객체가 들어가고

두번째에는 Issue를 생성할 수 있는 ctx객체가 제공됩니다.

 

ctx.addIssue() 함수에는

code, message, path를 전달해줄 수 있습니다.

code는 string 타입의 유니온이 전달됩니다.

위 예제와 같이 z.ZodIssueCode.custom으로 전달해주어도 괜찮고

code:'custom' 으로 전달해주어도 좋습니다.

 

code는 custom이외에도 여러가지 유니온이 있으니 궁금하신경우

타입스크립트 환경에서 확인해보시면 좋을 것 같습니다.

message에는 당연히 예상할 수 있듯이 에러 상황에 보여줄 메시지를 넣습니다.

 

그런데 여기까지 보면 의문이 생깁니다.

그러면 우리가 superRefine에서 정의한 에러메시지는

어디에 저장되어 있고 어떻게 접근해야하는것이지?

 

그 답은 path: 프로퍼티에 있습니다.

path 프로퍼티에는 string[]이 전달되어야 합니다.

그리고 이 배열의 요소에 전달되어야할 것은 에러가 표시될 스키마키값입니다.

      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['password'],
      });

예컨대 이렇게 addIssue를 하게되면

if()문이 트리거되었을때 formState.errors.password.messages에

password not matched가 나타나게됩니다.

type LoginType = z.infer<typeof LoginSchema>;

z.infer를 통해 정의한 스키마의 타입을 뽑아내줍니다.

여기서 뽑아낸 타입은 useForm의 제네릭으로 사용해줄 것입니다.


React Hook Form에서 사용해보자

  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm<LoginType>({
    resolver: zodResolver(LoginSchema),
  });

zod와 react-hook-form을 통합하는 것은 간단합니다.

위에서 정의해준 스키마를

zodResolver의 인수로 넣어주면끝입니다.

조금 헷갈리는 부분이 있을 수 있는데

formState:{errors}

이렇게 구조분해할당을 진행하는것은

formState.errors를 errors로 가져오는것을 의미합니다.

formState에는 말그대로 form의 현재 상태가 담겨있습니다.

이 errors객체에는 우리가 전달해주는 key값이 존재하게됩니다.

    <div className=" flex flex-col bg-sky-600">
      <h2 className=" text-white">이쨰륀</h2>
      <form
        className=" flex flex-col"
        onSubmit={handleSubmit((data) => console.log(data))}
      >
        <div>
          <label>이메일</label>
          <input
            type="text"
            {...register('email', {
              onChange: (e) => console.log(e.target.value),
            })}
            placeholder=" email"
          />
          {errors.email?.message && <span>{errors.email?.message}</span>}
        </div>
        <div>
          <label htmlFor="">이름</label>
          <input type="text" {...register('name')} />
        </div>
        <div>
          <label htmlFor="">비밀번호</label>
          <input type="password" {...register('password')} />
          {errors.password?.message && <span>{errors.password?.message}</span>}
        </div>
        <div>
          <label htmlFor="">비밀번호확인</label>
          <input type="password" {...register('checkPassword')} />
          {errors.checkPassword?.message && (
            <span>{errors.checkPassword?.message}</span>
          )}
        </div>
        <div>
          <label htmlFor="">폰번호</label>
          <input type="text" {...register('phone')} />
        </div>
        <input type="submit" />
      </form>
    </div>

이제 html 영역을 정의해주겠습니다.

한번에 모든것을 정의하다보니 조금 지저분하게 보이기도하네요

조금 특이한 부분이 input 의 type에 'email'을 전달해주게되면

zod의 email 밸리데이션보다 input의 에러메시지가 먼저 등장하게되어

zod의 에러메시지가 나오지 않더라구요 원인은 잘 모르겠읍니다.

{errors.password?.message && <span>{errors.password?.message}</span>}

이런 형식으로 errors.password?.message에 값이 truthy 할때에만

메시지를 렌더링하는 코드를 작성해주면 됩니다.

쉽게 눈치채실 수 있듯이 인풋로직에 반복되는 부분이 많다보니

코드를 줄일 수 있겠네요!

그래서 엑조디아처럼 모든 코드를 합친 형태로 보면


전체코드

'use client';
import z from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';

interface LoginFormProps {}

const phoneRegex = new RegExp(/^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/);

const LoginSchema = z
  .object({
    email: z
      .string()
      .min(1, { message: 'This field has to be filled' })
      .email({ message: 'Invalid email address' }),
    name: z.string().min(2).max(10),
    password: z.string().min(8),
    checkPassword: z.string().min(8),
    phone: z.string().regex(phoneRegex, 'Invalid PhoneNumber'),
  })
  .superRefine(({ checkPassword, password }, ctx) => {
    if (checkPassword !== password) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['checkPassword'],
      });
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'password not matched',
        path: ['password'],
      });
    }
  });

type LoginType = z.infer<typeof LoginSchema>;

const LoginForm = ({}: LoginFormProps) => {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm<LoginType>({
    resolver: zodResolver(LoginSchema),
  });
  return (
    <div className=" flex flex-col bg-sky-600">
      <h2 className=" text-white">이쨰륀</h2>
      <form
        className=" flex flex-col"
        onSubmit={handleSubmit((data) => console.log(data))}
      >
        <div>
          <label>이메일</label>
          <input
            type="text"
            {...register('email', {
              onChange: (e) => console.log(e.target.value),
            })}
            placeholder=" email"
          />
          {errors.email?.message && <span>{errors.email?.message}</span>}
        </div>
        <div>
          <label htmlFor="">이름</label>
          <input type="text" {...register('name')} />
        </div>
        <div>
          <label htmlFor="">비밀번호</label>
          <input type="password" {...register('password')} />
          {errors.password?.message && <span>{errors.password?.message}</span>}
        </div>
        <div>
          <label htmlFor="">비밀번호확인</label>
          <input type="password" {...register('checkPassword')} />
          {errors.checkPassword?.message && (
            <span>{errors.checkPassword?.message}</span>
          )}
        </div>
        <div>
          <label htmlFor="">폰번호</label>
          <input type="text" {...register('phone')} />
        </div>
        <input type="submit" />
      </form>
    </div>
  );
};

export default LoginForm;

이런 형태가 됩니다.

register()함수의 첫번째에는 인풋을 식별할 key값이 들어가게됩니다!


마치며

까다로운 유효성 검증을 선언 적으로 관리할 수 있다는게 매력적인 것 같습니다.

반응형
저작자표시 비영리

'react' 카테고리의 다른 글

react-query를 이용해 pagenation을 구현해보자  (0) 2023.06.29
forwardRef를 이용해 ref를 다는데... 타입은 어떻게 함?  (1) 2023.06.19
zod 라이브러리 사용법 간단히 익히기  (0) 2023.06.15
msw를 이용해 데이터 모킹하고 Suspense와 함께 react-query로 가져오기  (3) 2023.06.12
[react-query] react-query의 query key는...  (0) 2023.06.06
  1. zod는
  2. 작성해보기
  3. React Hook Form에서 사용해보자
  4. 마치며
'react' 카테고리의 다른 글
  • react-query를 이용해 pagenation을 구현해보자
  • forwardRef를 이용해 ref를 다는데... 타입은 어떻게 함?
  • zod 라이브러리 사용법 간단히 익히기
  • msw를 이용해 데이터 모킹하고 Suspense와 함께 react-query로 가져오기
냠냠맨
냠냠맨
프론트엔드 개발 전반을 다루는 기술 블로그입니다.
냠냠맨
React와 TypeScript를 좋아하는 개발자
냠냠맨
전체
오늘
어제
  • all category (433)
    • CMC (0)
    • best (11)
    • 년간회고 (1)
    • cheetsheet (15)
    • 프로젝트 회고 (3)
    • 서평 (3)
    • SEO Study (1)
    • 프로젝트 진행기 (10)
    • testcode (9)
    • yarnberry (7)
    • css (21)
    • typescript (15)
    • redux (7)
    • react (43)
    • Next.js (9)
    • Nestjs (3)
    • javascript (44)
    • programmers (67)
    • leetcode (41)
    • frontend (41)
    • backjoon (1)
    • Next.js Beta Docs 번역 (12)
    • TIL (15)
      • html (3)
    • Network (12)
      • 간단 정리 시리즈 (2)
      • 질답 준비 (0)
    • 자료구조와 알고리즘 (2)
    • CS (4)
      • OS (1)
    • 취업준비 (2)
    • zoom websocket (2)
    • talk (6)
    • 면접대비 (1)
    • 코드스테이츠 프론트 (5)
    • 간헐적 회고 (17)

블로그 메뉴

  • leetcode
  • programmers
  • javascript
  • html
  • css

공지사항

인기 글

태그

  • 개발
  • border말풍선
  • teosprint
  • 프론트엔드
  • 테오의스프린트17기
  • 개발자
  • frontend
  • LeetCode
  • 말풍선
  • 코드스테이츠 #프론트엔드
  • 주니어개발자
  • CSS
  • JavaScript
  • 테오의스프린트

최근 댓글

최근 글

hELLO · Designed By 정상우.
냠냠맨
zod를 이용해 회원가입 폼을 만들어보기
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.