[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user