Zod + React Hook Form 통합
React Hook Form은 성능 우수한 폼 라이브러리이고, Zod는 TypeScript-first 스키마 검증 라이브러리입니다. 둘을 결합하면 스키마 하나로 타입 추론과 유효성 검사를 동시에 처리할 수 있습니다.
기본 설정
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Zod 스키마 정의
const signupSchema = z.object({
email: z.string()
.min(1, '이메일을 입력해주세요')
.email('올바른 이메일 형식이 아닙니다'),
password: z.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.regex(/[A-Z]/, '대문자를 포함해야 합니다')
.regex(/[0-9]/, '숫자를 포함해야 합니다'),
confirmPassword: z.string(),
age: z.coerce.number()
.min(14, '14세 이상만 가입 가능합니다')
.max(120, '올바른 나이를 입력해주세요'),
}).refine((data) => data.password === data.confirmPassword, {
message: '비밀번호가 일치하지 않습니다',
path: ['confirmPassword'],
});
// 스키마에서 타입 자동 추론
type SignupForm = z.infer<typeof signupSchema>;
export default function SignupPage() {
const {
register, handleSubmit, formState: { errors, isSubmitting },
} = useForm<SignupForm>({
resolver: zodResolver(signupSchema),
defaultValues: { email: '', password: '', confirmPassword: '' },
});
const onSubmit = async (data: SignupForm) => {
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(data),
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<button disabled={isSubmitting}>가입</button>
</form>
);
}
조건부 필드 검증
const orderSchema = z.discriminatedUnion('deliveryType', [
z.object({
deliveryType: z.literal('shipping'),
address: z.string().min(1, '주소를 입력하세요'),
zipCode: z.string().regex(/^\d{5}$/, '우편번호 5자리'),
}),
z.object({
deliveryType: z.literal('pickup'),
storeId: z.string().min(1, '매장을 선택하세요'),
}),
]);
// useForm에서 watch로 조건부 렌더링
const deliveryType = watch('deliveryType');
{deliveryType === 'shipping' && (
<>
<input {...register('address')} placeholder="주소" />
<input {...register('zipCode')} placeholder="우편번호" />
</>
)}
서버 사이드 재사용
// shared/schemas.ts — 클라이언트 + 서버 공유
export const createPostSchema = z.object({
title: z.string().min(2).max(200),
content: z.string().min(10),
tags: z.array(z.string()).max(5),
published: z.boolean().default(false),
});
// API Route에서 동일 스키마 사용
export async function POST(req: Request) {
const body = await req.json();
const result = createPostSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
// result.data는 타입 안전
await db.post.create({ data: result.data });
}
커스텀 에러 맵
// 전역 에러 메시지 한글화
z.setErrorMap((issue, ctx) => {
const map = {
too_small: '최소 ' + (issue).minimum + '자 이상 입력하세요',
too_big: '최대 ' + (issue).maximum + '자까지 가능합니다',
invalid_type: '올바른 형식이 아닙니다',
invalid_string: '올바른 형식이 아닙니다',
};
return { message: map[issue.code] || ctx.defaultError };
});
- Zod 스키마 하나로 프론트엔드와 백엔드 검증을 통합할 수 있습니다
z.infer로 타입을 자동 추론하여 중복 타입 정의를 제거합니다superRefine은 복잡한 교차 필드 검증에 적합합니다z.coerce는 HTML input의 문자열을 숫자/날짜로 자동 변환합니다
Zod와 React Hook Form의 조합은 TypeScript 프로젝트에서 폼 처리의 사실상 표준이 되었습니다. 스키마 중심 접근은 유효성 검사 로직을 한 곳에 집중시키고, 타입 안전성을 보장합니다.
댓글 0