stack-analysis
CHAPTER 8 / 90
읽기 약 2분
FUNCTION
폼 관리: React Hook Form + Zod
핵심 개념
register·handleSubmit·watch·Zod 스키마 — 복잡한 회원가입 폼.
본문
기본 패턴
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const SignupSchema = z.object({
email: z.string().email('올바른 이메일'),
password: z.string()
.min(8, '8자 이상')
.regex(/[A-Z]/, '대문자 포함')
.regex(/\d/, '숫자 포함'),
passwordConfirm: z.string(),
name: z.string().min(2).max(50),
age: z.number().int().min(13).max(130),
agreeToTerms: z.literal(true, { errorMap: () => ({ message: '약관 동의 필수' }) }),
}).refine(data => data.password === data.passwordConfirm, {
message: '비밀번호 불일치',
path: ['passwordConfirm'],
});
type SignupForm = z.infer<typeof SignupSchema>;
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
watch,
} = useForm<SignupForm>({
resolver: zodResolver(SignupSchema),
mode: 'onBlur',
});
const password = watch('password');
const onSubmit = async (data: SignupForm) => {
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(data),
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} type="email" />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('password')} type="password" />
{errors.password && <p>{errors.password.message}</p>}
<input {...register('passwordConfirm')} type="password" />
{errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}
<input {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
<input {...register('age', { valueAsNumber: true })} type="number" />
{errors.age && <p>{errors.age.message}</p>}
<label>
<input {...register('agreeToTerms')} type="checkbox" />
약관 동의
</label>
{errors.agreeToTerms && <p>{errors.agreeToTerms.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '가입 중...' : '가입'}
</button>
</form>
);
}조건부 필드
const Schema = z.object({
type: z.enum(['individual', 'business']),
name: z.string(),
// 사업자만
businessNumber: z.string().optional(),
}).refine(data => {
if (data.type === 'business' && !data.businessNumber) return false;
return true;
}, { message: '사업자번호 필수', path: ['businessNumber'] });
function ConditionalForm() {
const { register, watch } = useForm<z.infer<typeof Schema>>();
const type = watch('type');
return (
<form>
<select {...register('type')}>
<option value="individual">개인</option>
<option value="business">사업자</option>
</select>
<input {...register('name')} />
{type === 'business' && (
<input {...register('businessNumber')} placeholder="사업자번호" />
)}
</form>
);
}파일 업로드
const Schema = z.object({
profile: z.instanceof(FileList)
.refine(files => files.length === 1, '파일 1개 필수')
.refine(files => files[0]?.size <= 5 * 1024 * 1024, '5MB 이하')
.refine(
files => ['image/jpeg', 'image/png'].includes(files[0]?.type),
'JPEG/PNG만'
),
});
function FileForm() {
const { register, formState: { errors } } = useForm<z.infer<typeof Schema>>();
return (
<div>
<input type="file" accept="image/*" {...register('profile')} />
{errors.profile && <p>{(errors.profile as any).message}</p>}
</div>
);
}동적 필드 (useFieldArray)
import { useFieldArray } from 'react-hook-form';
const Schema = z.object({
addresses: z.array(z.object({
street: z.string(),
city: z.string(),
})).min(1, '주소 1개 이상'),
});
function DynamicAddresses() {
const { control, register } = useForm<z.infer<typeof Schema>>({
defaultValues: { addresses: [{ street: '', city: '' }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: 'addresses',
});
return (
<div>
{fields.map((field, i) => (
<div key={field.id}>
<input {...register(`addresses.${i}.street`)} placeholder="주소" />
<input {...register(`addresses.${i}.city`)} placeholder="도시" />
<button type="button" onClick={() => remove(i)}>삭제</button>
</div>
))}
<button type="button" onClick={() => append({ street: '', city: '' })}>
주소 추가
</button>
</div>
);
}다음 챕터
CH.9 "컴포넌트 패턴" — Compound·Render Props.
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 폼 검증 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
폼 검증 관련 인기 라이브러리/패턴 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 폼 검증 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 프론트엔드 시장의 폼 검증 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
폼 관리: React Hook Form + Zod는 이 3가지만 확실히 잡으세요
1.React Hook Form + Zod = 타입 안전 + 빠름(uncontrolled) + 검증 강력
2.조건부 필드·파일·동적 배열 모두 지원 — 복잡 폼도 우아하게
3.다음 챕터 CH.9에서 컴포넌트 패턴 — 재사용성 극대화
공유하기
진행도 8 / 90