---
id: react-form-state-patterns
title: React Form 상태 패턴
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [react, forms, validation, react-hook-form, vibe-coding]
tech_stack: { language: "TypeScript / React 18+ / react-hook-form / zod", applicable_to: ["Web"] }
applied_in: []
aliases: [form library, validation schema, dirty fields]
---

# React Form 상태 패턴

> 큰 form 에서 매 keystroke setState 면 재렌더 폭주. **react-hook-form (uncontrolled + ref)** 또는 **Server Action + form data** 가 답. 검증은 zod 스키마.

## 📖 핵심 개념
form 의 어려움: 검증 / dirty 추적 / 제출 / 에러 표시 / async 검증 / 의존 필드. 직접 구현하면 복잡 + 성능 함정.

## 💻 코드 패턴

### 1. react-hook-form + zod
```tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  age: z.coerce.number().int().min(13),
});
type FormData = z.infer<typeof schema>;

function SignupForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } =
    useForm<FormData>({ resolver: zodResolver(schema) });

  return (
    <form onSubmit={handleSubmit(async data => { await api.signup(data); })}>
      <input {...register('email')} />
      {errors.email && <p>{errors.email.message}</p>}
      <input {...register('age')} type="number" />
      <button disabled={isSubmitting}>제출</button>
    </form>
  );
}
```

### 2. Next.js Server Action
```tsx
// 'use server'
async function signup(formData: FormData) {
  const parsed = schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { error: parsed.error.flatten() };
  await db.users.create(parsed.data);
}

// Client (또는 server-rendered form)
<form action={signup}>...</form>
```

## 🤔 의사결정 기준
| form 크기 / 복잡도 | 권장 |
|---|---|
| 1~3 input, 검증 단순 | useState controlled |
| 5+ input, 다중 검증 | react-hook-form + zod |
| Server-side validation 강제 | Server Action + zod (서버에서 재검증 필수) |
| 다단계 wizard | react-hook-form + form context, 또는 zustand |
| 동적 필드 추가/제거 | useFieldArray |

## ❌ 안티패턴
- **모든 input 에 useState**: 매 keystroke 전체 form 재렌더.
- **클라이언트 검증만 신뢰**: 서버에서 반드시 재검증. 클라는 UX, 서버는 진실.
- **중복 schema** (TS interface + 별도 validator): zod 의 z.infer 로 single source.
- **submit 후 input 안 비움**: form.reset() 호출.
- **disabled button 만으로 다중 제출 막기**: 빠른 더블 클릭에 race. Idempotency key 또는 mutation queue.
- **에러 메시지를 toast로만**: 어떤 필드가 문제인지 보여야 함. 인라인.

## 🤖 LLM 활용 힌트
- "react-hook-form + zod 조합으로 작성, 검증은 클라+서버 둘 다" 명시.
- 큰 form 일 때 LLM이 useState 다발 만들면 react-hook-form 으로 전환 요청.

## 🔗 관련 문서
- [[React_Controlled_vs_Uncontrolled]]
- [[Idempotent_Operations]]
