4.1 KiB
4.1 KiB
12. 폼 검증 (Zod)
대상: 프론트엔드 개발자 최종 업데이트: 2026-03-20
목차
| 번호 | 항목 |
|---|---|
| 12.1 | 적용 범위 |
| 12.2 | 기본 패턴 |
| 12.3 | 트러블슈팅 |
| 12.4 | 체크리스트 |
12.1 적용 범위
| 대상 | Zod 적용 |
|---|---|
| 신규 폼 | 필수 |
| 기존 폼 (정상 작동 중) | 건드리지 않음 |
| 단순 필드 1-2개 인라인 폼 | 불필요 (오버엔지니어링) |
| 신규 서버 액션 API 응답 | 선택적 |
12.2 기본 패턴
스키마 정의 + 타입 추출
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
// 1. 스키마 정의 (타입 + 검증 한 번에)
const formSchema = z.object({
itemName: z.string().min(1, '품목명을 입력하세요'),
quantity: z.number().min(1, '1 이상 입력하세요'),
status: z.enum(['active', 'inactive']),
memo: z.string().optional(),
});
// 2. 스키마에서 타입 추출 (별도 interface 정의 불필요)
type FormData = z.infer<typeof formSchema>;
// 3. useForm에 zodResolver 연결
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: { itemName: '', quantity: 1, status: 'active' },
});
규칙
- 스키마 위치: 컴포넌트 파일 상단 또는 같은 디렉토리의
schema.ts - 타입 추출:
z.infer<typeof schema>사용, 별도interface중복 정의 금지 - 에러 메시지: 한글로 작성 (사용자에게 직접 표시됨)
as캐스트 지양: Zod 스키마로 타입이 보장되므로 불필요
12.3 트러블슈팅
문제 1: 에러 메시지가 영어로 나옴
// ❌ 기본 에러 메시지 (영어)
const schema = z.object({
name: z.string().min(1),
});
// -> "String must contain at least 1 character(s)"
// ✅ 한글 메시지 명시
const schema = z.object({
name: z.string().min(1, '이름을 입력하세요'),
});
문제 2: 불필요한 필드까지 검증됨
수정 폼에서 등록 전용 필드가 검증되는 경우:
// ✅ .omit()으로 불필요 필드 제외
const editSchema = createSchema.omit({ password: true });
문제 3: 조건부 필수 필드
특정 값 선택 시에만 다른 필드가 필수인 경우:
// ✅ .superRefine() 사용
const schema = z.object({
type: z.enum(['individual', 'company']),
companyName: z.string().optional(),
}).superRefine((data, ctx) => {
if (data.type === 'company' && !data.companyName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '법인명을 입력하세요',
path: ['companyName'],
});
}
});
문제 4: .omit() 후 refinement 소실
.omit()은 .refine(), .superRefine() 등 체이닝된 검증을 제거합니다.
// ❌ refinement가 사라짐
const base = z.object({ a: z.string(), b: z.string() })
.refine((d) => d.a !== d.b, { message: 'a와 b는 달라야 합니다' });
const partial = base.omit({ b: true }); // refine 소실!
// ✅ .omit() 후 refinement 재적용
const partial = base.omit({ b: true });
const withRefine = partial.refine(...); // 필요 시 다시 추가
문제 5: 폼 필드명과 스키마 키 불일치
// ❌ 폼에서는 camelCase, 스키마에서는 snake_case
const schema = z.object({ item_name: z.string() });
// useForm에서 register('itemName') -> 검증 안 됨
// ✅ 동일한 키 사용
const schema = z.object({ itemName: z.string() });
12.4 체크리스트
신규 폼 작성 시 확인:
| # | 항목 |
|---|---|
| 1 | 스키마 정의 완료 (모든 필드 포함) |
| 2 | z.infer로 타입 추출 (별도 interface 없음) |
| 3 | zodResolver 연결 |
| 4 | 에러 메시지 한글 작성 |
| 5 | 조건부 필수 -> .superRefine() 사용 |
| 6 | 수정 폼 -> .omit() 적용 시 refinement 재확인 |
| 7 | 스키마 키와 폼 필드명 일치 확인 |
| 8 | as 캐스트 제거 |