react-hook-form
프로젝트 구현중에
로그인과 회원가입 폼에 관련해서
input추가시마다 state선언 및 관리를 많이 해야 하고,
그만큼 단순 반복 로직이 길어진다고 느꼈고 효율적이지 못해 보였다.
가독성도 떨어져보였다.
이것이 최선일까? 하는 의문이 들었다.
그리고 이것이 변경될 때마다 렌더링이 되는 점도 아쉬웠다.
react-hook-form의 가장 큰 장점은 ref로 스테이트를 관리하기 때문에
사용자가 입력시마다 리렌더링이 발생하지 않는다는 것이었지만
나는 결국 에러메시지를 바로바로 반영해주는 것이
사용자 관점에서 맞다고 생각해서 mode를 onChange로 변경하긴 했다.
또하나의 큰 장점은 validation 로직을 구현하는 것이라고 생각했다.
유효성 검증 로직을 따로 만들어서 그걸 입력할 때마다 실행해야 했었으나,
react-hook-form은 아래와 같이 입력하기만 하면
검증로직을 등록해주고
해당 로직에 맞지 않으면 error를 내보내준다
<Input
variant={errors.email ? 'danger' : 'passed'}
type="text"
placeholder="이메일 (이메일 형식)"
{...register('email', {
required: '반드시 입력해주세요',
pattern: {
value:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
message: '이메일 형식에 맞지 않습니다.',
},
})}
/>
input을 추가하려고 한다면 register에 이름을 명시해서 등록해주면 되고,
에러는 해당 이름의 키값으로 나오게 된다.
{errors.email && (
<p className="text-red-500">{errors.email.message}</p>
)}
zod
현재 프로젝트에서 타입스크립트를 쓰고 있는데 타입스크립트는 컴파일 시점의 타입 에러만 잡을 수 있다.
만일 컴파일 시점에서 잡더라도 런타임에서는 타입에러를 잡을 수 없는게 단점이다.
이것을 커버할 수 있는 것이 zod다.
react-hook-form으로 유효성검증 및 렌더링 이슈를 해결하기 위해 도입하던 도중
zod라는 라이브러리를 알게되었고 해당 라이브러리를 활용하여
런타임시점 에러를 함께 잡고,
패스워드 일치 검증 로직까지 함께 쉽게 구현할 수 있었다.
zod의 장점은 이뿐만이 아니라 따로 type을 선언할 필요 없이
zod object에 type을 검증하는 로직을 작성해두면
해당 type으로 추론할 수 있게 된다.
스키마 작성
const LoginSchema = z
.object({
name: z.string().min(2, { message: '2글자 이상 입력해주세요' }),
email: z
.string()
.min(1, { message: '반드시 입력해주세요' })
.email({ message: '이메일 형식에 맞게 입력해주세요' }),
password: z
.string()
.min(5)
.max(20)
.regex(
pwdRegex,
'영어소문자, 숫자 포함 5자 이상 20자 미만으로 입력해주세요',
),
checkPwd: z.string().min(5),
})
.superRefine(({ checkPwd, password }, ctx) => {
if (checkPwd !== password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '패스워드가 일치하지 않습니다.',
path: ['checkPwd'],
});
}
});
해당 스키마를 바탕으로 react-hook-form을 사용하여 form을 작성
const LoginForm = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isValid },
reset,
} = useForm<LoginType>({
mode: 'onChange',
resolver: zodResolver(LoginSchema),
defaultValues: {
name: '',
email: '',
password: '',
checkPwd: '',
},
});
return (
<Input
variant={errors.name ? 'danger' : 'passed'}
type="text"
placeholder="이름 (2글자 이상)"
{...register('name', {
required: '반드시 입력해주세요',
minLength: { value: 2, message: '2글자 이상 입력해주세요.' },
})}
/>
(...생략)