# 12. 폼 검증 (Zod) > 대상: 프론트엔드 개발자 > 최종 업데이트: 2026-03-20 --- ## 목차 | 번호 | 항목 | |------|------| | 12.1 | [적용 범위](#121-적용-범위) | | 12.2 | [기본 패턴](#122-기본-패턴) | | 12.3 | [트러블슈팅](#123-트러블슈팅) | | 12.4 | [체크리스트](#124-체크리스트) | --- ## 12.1 적용 범위 | 대상 | Zod 적용 | |------|---------| | 신규 폼 | 필수 | | 기존 폼 (정상 작동 중) | 건드리지 않음 | | 단순 필드 1-2개 인라인 폼 | 불필요 (오버엔지니어링) | | 신규 서버 액션 API 응답 | 선택적 | --- ## 12.2 기본 패턴 ### 스키마 정의 + 타입 추출 ```typescript 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; // 3. useForm에 zodResolver 연결 const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { itemName: '', quantity: 1, status: 'active' }, }); ``` ### 규칙 - **스키마 위치**: 컴포넌트 파일 상단 또는 같은 디렉토리의 `schema.ts` - **타입 추출**: `z.infer` 사용, 별도 `interface` 중복 정의 금지 - **에러 메시지**: 한글로 작성 (사용자에게 직접 표시됨) - **`as` 캐스트 지양**: Zod 스키마로 타입이 보장되므로 불필요 --- ## 12.3 트러블슈팅 ### 문제 1: 에러 메시지가 영어로 나옴 ```typescript // ❌ 기본 에러 메시지 (영어) 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: 불필요한 필드까지 검증됨 수정 폼에서 등록 전용 필드가 검증되는 경우: ```typescript // ✅ .omit()으로 불필요 필드 제외 const editSchema = createSchema.omit({ password: true }); ``` ### 문제 3: 조건부 필수 필드 특정 값 선택 시에만 다른 필드가 필수인 경우: ```typescript // ✅ .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()` 등 체이닝된 검증을 제거합니다. ```typescript // ❌ 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: 폼 필드명과 스키마 키 불일치 ```typescript // ❌ 폼에서는 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` 캐스트 제거 |