# 05. 폼 패턴 > **대상**: 프론트엔드 개발자 > **버전**: 1.0.0 > **최종 수정**: 2026-03-09 --- ## 1. 폼 패턴 2가지 | 패턴 | 적용 대상 | 비고 | |------|----------|------| | Zod + react-hook-form | **신규 폼** | 필수 | | useState + 수동 검증 | 기존 폼 | 건드리지 않음 | --- ## 2. 신규 폼: Zod + react-hook-form ### 기본 패턴 ```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 연결 const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(formSchema), defaultValues: { itemName: '', quantity: 1, status: 'active', }, }); ``` ### 규칙 | 항목 | 규칙 | |------|------| | 스키마 위치 | 컴포넌트 파일 상단 또는 같은 폴더의 `schema.ts` | | 타입 추출 | `z.infer` 사용, 별도 interface 중복 금지 | | 에러 메시지 | **한글** (사용자에게 직접 표시) | | `as` 캐스트 | 지양 (Zod가 타입 보장) | ### Zod 사용하지 않는 경우 - 기존 `rules={{ required: true }}` 패턴으로 작동 중인 폼 - 필드 1~2개짜리 인라인 폼 (오버엔지니어링) --- ## 3. FormField molecule Label + Input 수동 조합 대신 `FormField` 사용 (신규 폼). ### 기본 사용법 ```typescript import { FormField } from '@/components/molecules/FormField'; handleChange('companyName', value)} placeholder="회사명을 입력하세요" disabled={!isEditMode} /> ``` ### 지원 타입 | type | 설명 | 비고 | |------|------|------| | `text` | 일반 텍스트 (기본값) | | | `number` | 숫자 입력 | | | `email` | 이메일 | | | `tel` | 전화번호 | 자동 포맷 (010-1234-5678) | | `businessNumber` | 사업자등록번호 | 자동 포맷 (123-45-67890) | | `textarea` | 여러 줄 텍스트 | | | `currency` | 금액 입력 | 콤마 자동 포맷 | | `select` | 드롭다운 선택 | options prop 필요 | | `date` | 날짜 선택 | DatePicker 연동 | ### FormField로 대체하지 않는 경우 - Select, DatePicker 단독 사용 (이미 Label 포함인 경우) - ImageUpload, FileInput 등 특수 컴포넌트 - 복합 레이아웃 (주소 검색: 버튼+입력 조합) ### 비교 ```typescript // ✅ FormField 사용 (신규 폼) handleChange('companyName', value)} /> // ❌ 수동 조합 (신규 폼에서 금지)
handleChange('companyName', e.target.value)} />
``` --- ## 4. 기존 폼 패턴 (수정하지 않음) ```typescript // useState 기반 — 작동 중이면 건드리지 않음 const [formData, setFormData] = useState(initialData); const handleChange = (field: keyof FormData, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); }; // react-hook-form rules 기반 ``` --- ## 5. 서버 검증 에러 처리 Laravel에서 반환하는 validation 에러를 폼에 표시: ```typescript const result = await saveItem(formData); if (!result.success && result.fieldErrors) { // fieldErrors: { name: ['이름은 필수입니다.'], amount: ['0보다 커야 합니다.'] } Object.entries(result.fieldErrors).forEach(([field, messages]) => { setError(field as keyof FormData, { type: 'server', message: messages[0], }); }); } ``` --- ## 6. DatePicker 사용 규칙 `input type="date"` 대신 커스텀 DatePicker 사용: ```typescript import { DatePicker } from '@/components/ui/date-picker'; setFormData(prev => ({ ...prev, startDate: date }))} placeholder="날짜 선택" /> ``` - `value`/`onChange`: string (`yyyy-MM-dd`) - `minDate`/`maxDate`: Date 객체 (`new Date('2026-01-01')`) - 테이블 셀: `size="sm"` 사용 - 한글 로케일, 주말/공휴일 색상 구분 --- ## 7. Radix UI Select 주의사항 빈 값('')으로 시작 후 값 변경이 안 되는 버그 → `key` prop으로 해결: ```typescript // ✅ key prop으로 강제 리마운트 ```