[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-10 22:08:15 +09:00
parent 21ac3ed255
commit 504fd5fb42
3011 changed files with 380280 additions and 206977 deletions
@@ -0,0 +1,449 @@
---
id: frontend-form-state-deep
title: Form State — RHF / TanStack Form / Conform
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [frontend, form, react, vibe-coding]
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
applied_in: []
aliases: [react-hook-form, RHF, TanStack Form, Conform, Formik, Zod, validation, form library]
---
# Form State Library
> Form state 가 React 의 가장 boilerplate. **react-hook-form (가장 인기), TanStack Form (modern), Conform (server action 친화)**. Zod / Yup 가 schema.
## 📖 핵심 개념
- Uncontrolled (RHF) > controlled (re-render ↓).
- Schema validation (Zod / Yup / Valibot).
- Async validation.
- Field array (dynamic list).
## 💻 코드 패턴
### react-hook-form (RHF)
```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(),
password: z.string().min(8),
});
type FormData = z.infer<typeof schema>;
function LoginForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
await api.login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
<button disabled={isSubmitting}>Submit</button>
</form>
);
}
```
→ Uncontrolled (DOM 가 source). Re-render 적음.
### Field array (dynamic)
```tsx
const { register, control } = useForm<{ items: { name: string }[] }>();
const { fields, append, remove } = useFieldArray({ control, name: 'items' });
return (
<>
{fields.map((field, i) => (
<div key={field.id}>
<input {...register(`items.${i}.name`)} />
<button onClick={() => remove(i)}>Delete</button>
</div>
))}
<button onClick={() => append({ name: '' })}>Add</button>
</>
);
```
### Watch (subscribe)
```tsx
const { watch } = useForm();
const password = watch('password');
// Watch all
const all = watch();
```
→ 매 변경 = re-render. 사용 자제.
### `useController` (Custom field)
```tsx
import { useController } from 'react-hook-form';
function CustomInput({ control, name }) {
const { field, fieldState } = useController({ control, name });
return <input {...field} />;
}
```
→ Class library / 외부 component (MUI 등).
### Async validation
```ts
const schema = z.object({
email: z.string().email().refine(async (email) => {
const exists = await api.checkEmail(email);
return !exists;
}, 'Email already exists'),
});
```
→ Zod 가 async refine.
### Default values (async)
```tsx
const { reset } = useForm({ defaultValues: async () => fetchUser() });
// → 자동 reset 후 정착.
```
### Submit error
```tsx
const onSubmit = async (data) => {
try {
await api.login(data);
} catch (e) {
setError('email', { message: 'Wrong credentials' });
}
};
```
### TanStack Form (modern)
```tsx
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';
const form = useForm({
defaultValues: { email: '', password: '' },
onSubmit: async ({ value }) => {
await api.login(value);
},
validatorAdapter: zodValidator,
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.Field
name="email"
validators={{ onBlur: z.string().email() }}
children={(field) => (
<>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors.length > 0 && <p>{field.state.meta.errors[0]}</p>}
</>
)}
/>
</form>
);
```
→ Modern, type-safe, framework-agnostic.
### Conform (server action 친화)
```tsx
'use client';
import { useForm } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { loginAction } from './actions';
export default function LoginForm() {
const [form, fields] = useForm({
onValidate({ formData }) {
return parseWithZod(formData, { schema: loginSchema });
},
});
return (
<form id={form.id} action={loginAction}>
<input name={fields.email.name} />
<p>{fields.email.errors}</p>
<button>Submit</button>
</form>
);
}
```
→ Server Action / Remix native.
### Zod schema
```ts
const userSchema = z.object({
email: z.string().email().min(1, 'Required'),
age: z.number().int().min(18).max(100),
role: z.enum(['admin', 'user']),
bio: z.string().optional(),
tags: z.array(z.string()).max(5),
}).refine(d => d.email !== 'admin@x.com', 'Not allowed');
type User = z.infer<typeof userSchema>;
```
### Conditional fields
```ts
const schema = z.discriminatedUnion('type', [
z.object({ type: z.literal('email'), email: z.string().email() }),
z.object({ type: z.literal('phone'), phone: z.string() }),
]);
```
```tsx
const type = watch('type');
{type === 'email' && <input {...register('email')} />}
{type === 'phone' && <input {...register('phone')} />}
```
### Multi-step form
```tsx
const [step, setStep] = useState(1);
const { register, handleSubmit, trigger } = useForm({ shouldUnregister: false });
const next = async () => {
// Validate current step
const valid = await trigger(['email']); // step 1 fields
if (valid) setStep(2);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{step === 1 && <input {...register('email')} />}
{step === 2 && <input {...register('password')} />}
<button type="button" onClick={next}>Next</button>
</form>
);
```
### Form persist (refresh 시)
```ts
useEffect(() => {
const saved = localStorage.getItem('form');
if (saved) reset(JSON.parse(saved));
}, []);
const values = watch();
useEffect(() => {
localStorage.setItem('form', JSON.stringify(values));
}, [values]);
```
### Optimistic UI
```tsx
const onSubmit = async (data) => {
// Local update 즉시
setItems([...items, data]);
try {
await api.create(data);
} catch {
// Rollback
setItems(items);
}
};
```
### Server Action (RSC)
```tsx
// app/actions.ts
'use server';
import { z } from 'zod';
const schema = z.object({ email: z.string().email() });
export async function loginAction(formData: FormData) {
const data = schema.parse(Object.fromEntries(formData));
await db.users.create(data);
redirect('/dashboard');
}
```
```tsx
// app/login/page.tsx
import { loginAction } from '../actions';
export default function Page() {
return (
<form action={loginAction}>
<input name="email" />
<button>Submit</button>
</form>
);
}
```
→ React 19 / Next.js. JS 없이도 form 가 work.
### useActionState (React 19)
```tsx
'use client';
import { useActionState } from 'react';
const [state, action, isPending] = useActionState(loginAction, null);
return (
<form action={action}>
<input name="email" />
{state?.error && <p>{state.error}</p>}
<button disabled={isPending}>Submit</button>
</form>
);
```
→ Server action + client UX.
### Performance
```
Controlled (useState): 매 keystroke = re-render.
Uncontrolled (RHF): blur / submit 만 re-render.
→ 큰 form (50+ field) 가 RHF 의 큰 차이.
```
### Validation timing
```
- onBlur (default): user 가 떠나면.
- onChange: 매 keystroke (긴 form 에 X).
- onSubmit: submit 만.
→ Field 별 다름:
- Email: onBlur.
- Password strength: onChange.
- Form-level: onSubmit.
```
### Accessibility
```tsx
<input
{...register('email')}
aria-invalid={!!errors.email}
aria-describedby="email-error"
/>
{errors.email && (
<p id="email-error" role="alert">{errors.email.message}</p>
)}
```
→ Screen reader 가 error 읽음.
### Library 비교
```
RHF: 가장 인기, mature, ecosystem 큰.
TanStack Form: modern, framework-agnostic.
Conform: server action 친화.
Formik: legacy (RHF 가 추월).
```
### Validator 비교
```
Zod: TS-first, 큰 ecosystem.
Yup: legacy, JS 친화.
Valibot: 작은 bundle (10x 작음 보다 Zod), modern.
ArkType: 빠른 + complex.
Joi: Node-friendly.
```
### Custom validation (Zod)
```ts
const schema = z.object({
password: z.string()
.min(8)
.refine(s => /[A-Z]/.test(s), 'Need uppercase')
.refine(s => /\d/.test(s), 'Need digit')
.refine(s => /[!@#$]/.test(s), 'Need special'),
confirm: z.string(),
}).refine(d => d.password === d.confirm, {
message: 'Passwords mismatch',
path: ['confirm'],
});
```
### File upload
```tsx
const { register } = useForm<{ avatar: FileList }>();
<input type="file" {...register('avatar')} />
const onSubmit = ({ avatar }) => {
const file = avatar[0];
const formData = new FormData();
formData.append('file', file);
fetch('/upload', { method: 'POST', body: formData });
};
```
### Error from server
```ts
const onSubmit = async (data) => {
try {
await api.submit(data);
} catch (e) {
if (e.fields) {
Object.entries(e.fields).forEach(([name, msg]) => {
setError(name, { message: msg });
});
} else {
setError('root', { message: e.message });
}
}
};
```
## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| React + 일반 | react-hook-form + Zod |
| Modern type-safe | TanStack Form |
| Server action | Conform |
| 큰 bundle 안 됨 | Valibot |
| Multi-step | RHF + step state |
| File upload | RHF + FormData |
| Async validate | Zod refine async |
## ❌ 안티패턴
- **모든 거 controlled (useState 매 input)**: 큰 form = 느린.
- **No schema**: server / client validation 다름.
- **Validate onChange (긴 form)**: jittery.
- **No server-side validate**: 신뢰 X.
- **Watch all + render**: 폭발.
- **No accessibility**: a11y 깨짐.
- **No optimistic UI**: 느린 UX.
## 🤖 LLM 활용 힌트
- RHF + Zod 가 default React stack.
- TanStack Form 가 modern (framework-agnostic).
- Conform 가 server action 친화.
- Validator = Zod (TS) / Valibot (작은).
## 🔗 관련 문서
- [[React_Form_State_Patterns]]
- [[React_RHF_Zod_Patterns]]
- [[TS_Schema_Validation_Comparison]]