# 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<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: 에러 메시지가 영어로 나옴

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