Files
sam-react-prod/sam-docs/frontend/v1/12-form-validation.md

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 캐스트 제거