10편의 다중 input + 객체 state + if 검증 패턴은 입문용. 회원가입에 8개 필드, 검증 규칙 15개 들어가면 그 코드는 곧 통제 불가. 회사 stack 의 답 — React Hook Form (성능·DX) + Zod (스키마·타입 추론) 조합. 2026 년 React 폼의 사실상 표준.
이번 22편은 두 라이브러리의 정확한 역할 분담 + 통합 패턴 + 흔한 에러 시각화·서버 에러 처리까지.
1. 왜 둘이 짝인가
역할 분담 — Zod 는 스키마 정의 (필드·타입·규칙), RHF 는 폼 상태 관리 (input 값·에러·제출). RHF 단독이면 검증 규칙을 매번 함수로 작성. Zod 와 결합하면 스키마 1개로 (a) 런타임 검증 + (b) TypeScript 타입 자동 추론 + (c) RHF resolver 자동 연결.
2. 통합 셋업 — 5분
npm install react-hook-form zod @hookform/resolvers
// SignupForm.tsx
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// 1. 스키마 정의 (이게 진실의 원천)
const schema = z.object({
email: z.string().email('이메일 형식이 아닙니다'),
password: z.string().min(8, '8자 이상이어야 합니다'),
age: z.coerce.number().min(14, '14세 이상').max(120),
terms: z.boolean().refine(v => v, '약관 동의 필수'),
});
// 2. 타입 자동 추출 — 별도 type 작성 불필요
type FormData = z.infer<typeof schema>;
export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({ resolver: zodResolver(schema) });
const onSubmit = async (data: FormData) => {
// data 는 검증·변환 완료된 객체 (age 가 number 보장)
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('age')} type="number" />
{errors.age && <p>{errors.age.message}</p>}
<label><input {...register('terms')} type="checkbox" /> 약관 동의</label>
{errors.terms && <p>{errors.terms.message}</p>}
<button disabled={isSubmitting}>{isSubmitting ? '가입 중...' : '가입'}</button>
</form>
);
}
핵심 — register('email') 한 줄이 input 의 value·onChange·onBlur·ref 를 다 연결. 10편의 controlled input 패턴이 자동화된 셈. handleSubmit 이 검증 통과 시에만 onSubmit 호출.
3. RHF 의 성능 이점 — uncontrolled 기반
10편에서 controlled vs uncontrolled 비교했다. RHF 는 내부적으로 uncontrolled + ref 추적으로 동작. 매 키 입력에 재렌더 안 함 → 폼 필드 30개 짜리도 부드러움.
// 보통 controlled (느림)
const [name, setName] = useState('');
<input value={name} onChange={(e) => setName(e.target.value)} />
// 매 키 입력 → 컴포넌트 전체 재렌더
// RHF (빠름)
<input {...register('name')} />
// 매 키 입력 → DOM 만 갱신, React 재렌더 0
16편 성능 최적화에서 다룬 memoization 이 거의 필요 없어진다. 폼은 RHF 가 알아서 빠르게.
4. 서버 에러 + 흔한 패턴
클라이언트 검증 통과 → 서버 호출 → 서버가 "이미 가입된 이메일" 같은 에러 반환. RHF 의 setError 로 같은 에러 표시 시스템에 통합:
const { setError, handleSubmit } = useForm({...});
const onSubmit = async (data) => {
const res = await fetch('/api/signup', { ... });
if (!res.ok) {
const { field, message } = await res.json();
setError(field, { message }); // 해당 필드 에러로 표시
return;
}
navigate('/welcome');
};
실전 패턴 — 스키마 파일을 src/schemas/ 폴더에 분리. 서버 API (Next.js Route Handler 등) 와 같은 스키마를 import 해서 양쪽이 동일한 검증 규칙. "한 번 작성 = 클라이언트 + 서버 + 타입 모두 적용" 이 Zod 의 진짜 가치.
22편으로 React 의 모든 일상 패턴 완료. 23편부터는 미래 — React Server Components 가 SPA 모델을 어떻게 바꾸는지.
다음 글
React 교재 23편 — Server Components (RSC). use server·use client 경계, 데이터 fetch가 컴포넌트 안으로 들어오는 시대.