본문 바로가기
Frontend2025년 8월 12일7분 읽기

Zod + React Hook Form 타입 안전 폼 유효성 검사 심화

YS
김영삼
조회 722

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

아직 댓글이 없습니다.
Ctrl+Enter로 등록