zod는
타입스크립트를 우선하는 스키마 선언 / 검증 라이브러리입니다.
타입스크립트의 타입체킹을 보완해주는 역할을 수행한다고도 볼 수 있는데
쉽게 생각하면 폼,인풋에 필연적으로 따라오는 유효성검증 로직을 해결해주는 라이브러리입니다.
기본적인 개념과 사용방법은 아래 링크에서 참고해주시기 바랍니다.
https://xionwcfm.tistory.com/346
이번엔 위 링크에서 얻은 선수지식을 기반으로
사용할 수 있는 회원가입 로직을 작성해보겠습니다.
회원가입 로직에서 요구되는 사항은 다음과 같습니다.
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 |