OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 8 / 90
stack-analysis
CHAPTER 8 / 90
읽기 약 2
FUNCTION

폼 관리: React Hook Form + Zod


핵심 개념

register·handleSubmit·watch·Zod 스키마 — 복잡한 회원가입 폼.

본문

기본 패턴

TYPESCRIPT📋 코드 (71줄)
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>
  );
}

조건부 필드

TYPESCRIPT📋 코드 (30줄)
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>
  );
}

파일 업로드

TYPESCRIPT📋 코드 (20줄)
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)

TYPESCRIPT📋 코드 (35줄)
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