React 교재 · 고급 22편

React Zod + React Hook Form

타입 안전 폼의 표준. 회사 stack 에 둘 다 있는 이유는 명확하다.

검증 체크 마크가 필드 옆에 뜨는 폼 일러스트 — Zod+RHF 컨셉

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가 컴포넌트 안으로 들어오는 시대.

© 2026 주나이테크(주) @JUNAITECH