React Hook Form - performant, flexible and extensible form library
Performant, flexible and extensible forms with easy-to-use validation.
react-hook-form.com
1. React Hook Form (RHF) 이란?
✔ 핵심 정의 (상세)
React Hook Form(RHF)은
React의 함수형 컴포넌트에서 "Form 상태 관리", "검증", "에러 처리", "제출 로직"을 간단하고 성능 좋게 관리해주는 라이브러리입니다.
React Hook Form은 React의 Hooks 시스템을 기반으로 한 Form 관리 라이브러리 중 가장 널리 사용되는 표준 라이브러리입니다.
왜 React Hook Form인가?
기존 Form 관리의 문제점:
- useState로 모든 Input 상태를 관리 → 렌더링 비용 과다
- 입력 필드가 많을수록 상태 관리 복잡
- 검증, 에러 관리, submit, UX를 따로 코드에서 복잡하게 구현해야 함
React Hook Form의 접근:
- Uncontrolled Input 기반 (useRef 방식) → Input 자체가 DOM 상태를 갖고 있고, RHF는 ref만 연결
- 렌더링 최적화 → 상태 변경 시 Form 전체가 아니라 필요한 필드만 리렌더
- 검증, 에러, 제출 흐름이 모두 Hook 내부에서 깔끔하게 관리
- Form을 선언적으로 설계 → 코드 깔끔, 상태 관리 안정
핵심 특징
1. Uncontrolled input 방식 (ref 기반) → 성능 최적화
- React Hook Form은 input 필드를 DOM과 직접 연결해서 상태를 관리합니다.
- 그래서 모든 상태가 React state에 직접 들어가지 않고,
useRef를 통해 입력 필드의 값을 관리 → 상태 변화 시 리렌더 최소화
→ 큰 Form에서도 빠르고 부드러운 UX 유지 가능
<input {...register('email')} />
2. 내장 검증 + 외부 검증 스키마 지원
- 기본 제공하는 검증 (required, minLength 등)으로 빠르게 시작 가능
- 복잡한 비즈니스 검증이 필요하면 Zod, Yup 같은 스키마 검증 라이브러리와 통합 가능 (resolver)
3. React Query, Zustand 등과 강력 호환
- RHF는 단순히 Form 상태만 관리 →
서버 상태(React Query), 클라이언트 상태(Zustand, Recoil)와 자연스럽게 조합 가능 - 예를 들어:
- onSubmit에서 React Query mutation 호출
- RHF에서 입력한 데이터 → Zustand에 저장
- 상태 일관성 쉽게 유지
2. React Hook Form의 내부 동작 원리
✔ 기존 Form 관리 vs RHF 방식 비교
기존 Form 관리 (state 기반)
const [email, setEmail] = useState('');
<input value={email} onChange={(e) => setEmail(e.target.value)} />
- 입력할 때마다 → setEmail 호출 → React state 업데이트 →
→ 컴포넌트 전체 리렌더링 - 필드가 많아질수록 → 성능 저하, 복잡도 증가
- 모든 필드 상태, 에러, 검증 → 직접 관리 필요 (onChange, onBlur 직접 처리)
RHF 방식 (ref 기반, Uncontrolled Input)
const { register } = useForm();
<input {...register('email')} />
- React state 사용 ❌
- Input 자체는 DOM이 관리
- React Hook Form은 ref로 Input과 연결 → 내부적으로 value, error, touched 등을 ref 기반으로 추적
- 필요한 필드만 리렌더 → Form 전체 리렌더 X
핵심 동작 흐름 (시각적)
[Input 필드]
▼ (onChange)
[DOM 내부 상태 관리 (Uncontrolled)]
▼
[React Hook Form useRef로 추적]
▼
[submit → handleSubmit 호출 시 → 검증 → 상태 업데이트 → errors, isSubmitting 등 업데이트]
▼
[FormState만 리렌더 (필요한 부분만)]
내부 원리 핵심 3단계
| 1. 등록 | register('fieldName') → ref로 Input과 연결 |
| 2. 검증 | handleSubmit 시점에 내부에서 Input 값 수집 → 검증 수행 (내장 or 스키마) |
| 3. 상태 관리 | errors, touched, isSubmitting 등 FormState에서 관리 → 필요한 필드만 리렌더 |
✔ RHF가 성능이 좋은 이유
| Uncontrolled Input | Input의 값, 상태가 DOM 안에서 관리 (React가 직접 개입 X) |
| useRef 기반 추적 | Input을 ref로 연결 → 값 추적만 React Hook Form이 관리 |
| 최소 렌더링 | Form 전체 리렌더가 아닌 필요한 필드만 상태 업데이트 → 리렌더 |
| 상태 구독 최적화 | formState를 proxy로 관리 → 필요한 필드만 리렌더 감지 |
- 기존 방식: 매 입력마다 React에 보고 → 모든 컴포넌트 다시 그림
- RHF 방식: Input은 자체적으로 동작 → 필요한 시점(submit, onBlur, etc)에만 React Hook Form이介入 → 성능 최적
✔ 실무에서 알아야 할 동작 특성
- register() → Input과 연결 → 내부적으로 ref와 onChange, onBlur 등 자체 관리
- handleSubmit() → Form 검증 + onSubmit 호출
- formState.errors → 에러 상태 관리 (필요할 때만 리렌더)
- watch(), setValue(), reset() → ref 기반으로 Input 직접 제어 가능
3. RHF 주요 API 핵심 이해
1. register()
✔ 역할
- Input을 RHF에 등록 → ref 연결 → 자동으로 value, onChange, onBlur 관리
- 가장 핵심이자 가장 많이 쓰는 API
✔ 사용 예
<input {...register('email', { required: '이메일 필수' })} />
✔ 특징
- Input에 직접 props spread (...register('필드명'))
- 필드별 유효성 검증 옵션(required, minLength 등) 지정 가능
2. handleSubmit()
✔ 역할
- Form 제출을 감싸고 검증 수행 → 검증 성공 시 onSubmit 호출 → 실패 시 formState.errors 갱신
✔ 사용 예
<form onSubmit={handleSubmit((data) => console.log(data))}>
3. formState
✔ 역할
- Form의 상태, 에러, submit 상태 등 관리
✔ 자주 사용하는 속성
| errors | 검증 오류 객체 |
| isSubmitting | submit 진행 중 여부 (비동기) |
| isValid | 전체 검증 통과 여부 |
| touchedFields | 터치된 필드들 |
| dirtyFields | 변경된 필드들 |
✔ 사용 예
if (formState.errors.email) {
console.log(formState.errors.email.message);
}
4. setValue()
✔ 역할
- RHF 내부 상태에서 필드 값을 강제로 변경
- Input을 직접 변경할 필요 없이, Form 내부 상태만 업데이트 가능
✔ 사용 예
setValue('email', 'example@example.com');
✔ 특징
- 비동기 상태 변경 필요할 때, API 응답 → Form 자동 업데이트
- UI 동기화 유지 → validation 즉시 반영 가능
5. watch()
✔ 역할
- 특정 필드 값 실시간 구독
- 필드의 현재 값을 React state 없이 감시 가능
✔ 사용 예
const emailValue = watch('email');
✔ 특징
- 실시간 Preview, 동적 UI 제어에 유용
6. reset()
✔ 역할
- Form 상태 초기화 (필드 값, 에러, touched 전부 초기화 가능)
- API 응답을 받아서 Form 상태를 새로운 값으로 세팅할 때 유용
✔ 사용 예
reset({ email: 'admin@example.com' });
7. useFieldArray()
✔ 역할
- 동적 필드 관리 (예: 태그, 참여자 리스트 등 추가/삭제가 자유로운 배열 Form)
✔ 사용 예
const { fields, append, remove } = useFieldArray({ control, name: 'skills' });
fields.map((field, index) => (
<input {...register(`skills.${index}.name`)} key={field.id} />
));
✔ 특징
- RHF에서 가장 강력한 기능 중 하나 → 복잡한 Form에서도 성능 저하 없이 동적 필드 관리 가능
useFieldArray 옵션
✔ 기본 형태
const { fields, append, prepend, remove, swap, move, insert, update, replace } = useFieldArray({
control,
name: 'skills', // 필드 배열의 name (예: skills, emails)
keyName: 'id', // (선택) 기본 'id' → 필드 고유 key 커스터마이징 가능
});
✔ 주요 옵션
| control | useForm에서 반환한 control 필수 |
| name | 필드 배열의 이름 (skills, tags 등) |
| keyName | (선택) 기본 'id'. key 프로퍼티 이름 변경 시 사용 (필수는 아님, 성능 최적화 목적) |
실무 팁
- keyName을 기본 'id'로 두고 사용 권장 (React key 최적화)
- control은 반드시 useForm에서 받은 것을 그대로 넘겨야 정상 동작
반환 값 (API)
| fields | 현재 필드 배열 상태 ({ id, ...field }[]) |
| append(value) | 필드 마지막에 추가 |
| prepend(value) | 필드 맨 앞에 추가 |
| remove(index) | 특정 인덱스 필드 제거 |
| insert(index, value) | 특정 위치에 필드 삽입 |
| swap(indexA, indexB) | 두 필드 위치 교환 |
| move(from, to) | 필드를 원하는 위치로 이동 |
| update(index, value) | 특정 필드 값 직접 업데이트 |
| replace(data[]) | 전체 필드 배열 교체 |
실무 활용 예시
✔ 1. append
append({ name: '' });
✔ 2. remove
remove(1); // 두 번째 필드 삭제
✔ 3. swap
swap(0, 2); // 첫 번째와 세 번째 필드 위치 변경
✔ 4. insert
insert(1, { name: 'New Skill' }); // 두 번째 위치에 삽입
✔ 5. update
update(0, { name: 'Updated Skill' }); // 첫 번째 필드 값 업데이트
✔ 6. replace
replace([{ name: 'Skill A' }, { name: 'Skill B' }]); // 전체 리스트 교체
고급 팁
- fields는 불변 객체 아님 → React에서 key로 field.id 반드시 사용 (성능 최적화 필수)
- append, remove 호출 후 → RHF는 자동으로 register 및 검증, 상태 관리
- replace는 초기값 세팅, 서버에서 받은 리스트로 초기화할 때 매우 유용
- update를 사용하면 Form 상태만 업데이트 → UI 리렌더 X (성능 최적화용으로 강력)
요약
const { fields, append, remove, swap, move, insert, update, replace } = useFieldArray({
control,
name: 'skills',
});
- 추가: append(), prepend(), insert()
- 삭제: remove()
- 수정: update()
- 위치 변경: swap(), move()
- 전체 교체: replace()
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { tagFormSchema, TagFormRequest } from '@/schemas/tagForm';
export const TagForm = () => {
const { register, handleSubmit, control, formState: { errors, isSubmitting, isValid } } = useForm<TagFormRequest>({
resolver: zodResolver(tagFormSchema),
mode: 'onBlur',
reValidateMode: 'onChange',
defaultValues: {
tags: ['React'],
},
shouldUnregister: true,
});
const { fields, append, remove, replace } = useFieldArray({
control,
name: 'tags',
});
const onSubmit = (data: TagFormRequest) => {
console.log('제출 데이터:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`tags.${index}`)}
placeholder="태그 입력"
disabled={isSubmitting}
/>
<button type="button" onClick={() => remove(index)}>
삭제
</button>
{errors.tags?.[index] && <p>{errors.tags[index]?.message}</p>}
</div>
))}
<button type="button" onClick={() => append('')}>태그 추가</button>
<button type="button" onClick={() => replace(['React', 'TypeScript'])}>초기화</button>
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? '처리 중...' : '제출'}
</button>
</form>
);
};
| Input 연결 | register() |
| Form 제출 + 검증 | handleSubmit() |
| 상태 관리, 에러 추적 | formState |
| 필드 값 강제 변경 | setValue() |
| 필드 실시간 값 구독 | watch() |
| Form 초기화 | reset() |
| 동적 필드 관리 (배열) | useFieldArray() |
4. RHF 검증 흐름 종류
| HTML 표준 검증 | required, minLength 등 옵션 직접 사용 | 매우 간단한 Form |
| 커스텀 validator | validate 옵션으로 JS 함수 제공 | 실시간 검증 |
| 스키마 기반 | resolver (Yup, Zod 등) 사용 | 복잡, 실무 권장 |
1. 기본 Validator 방식 (HTML 속성 + register 옵션)
✔ 특징
- register 옵션으로 필드마다 간단한 검증 지정
- required, minLength, maxLength, pattern 등 HTML 표준 검증
- 가장 빠르고 간단하지만, 복잡한 검증은 어려움
✔ 코드 예시
<input
{...register('email', {
required: '이메일 필수',
minLength: { value: 5, message: '5자 이상' },
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '이메일 형식' }
})}
/>
✔ 실무 추천 상황
- 아주 간단한 Form (로그인, 검색 등)
- 빠르게 프로토타입 만들 때
2. 커스텀 Validator (validate 옵션)
✔ 특징
- validate 옵션으로 JS 함수 기반 자유로운 검증 가능
- 필드 값에 따라 동적 검증 가능
- 비동기 검증(async)도 지원 (단, resolver 사용하는 게 더 좋음)
✔ 코드 예시
<input
{...register('username', {
validate: (value) => value.length >= 3 || '3자 이상 입력'
})}
/>
✔ 실무 추천 상황
- 필드 단위에서만 간단한 custom 검증
- cross-field 검증이 필요한 경우는 resolver 추천
3. 스키마 기반 검증 (외부 라이브러리 + resolver)
✔ 특징
- Yup, Zod, Joi 같은 스키마 검증 라이브러리와 결합
- resolver를 통해 RHF와 연결 → 검증은 스키마가 담당 → Form은 검증 로직에서 자유로움
- 복잡한 검증, cross-field, 조건부 검증, API 기반 검증에 최적
- 검증, DTO, API 타입을 통합 관리 가능
✔ 코드 예시 (Zod 사용)
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
✔ 실무 추천 상황
- 복잡한 Form
- 대규모 프로젝트
- 검증 로직과 Form 코드를 완전히 분리하고 싶을 때
- API 요청 DTO와 검증을 통합 관리하고 싶을 때
5. 실무 통합 예제 — RHF + Zod + React Query + UX 최적화
전체 흐름 (시각적)
[사용자 입력 (Input)]
↓
[React Hook Form]
↓
[zodResolver → Zod 스키마 검증]
↓
[React Query mutation (signUpApi)]
↓
[onMutate → UI 상태 변경 (버튼 비활성화 등)]
↓
[onSuccess → 성공 알림, 페이지 이동]
↓
[onError → 에러 메시지 Toast, errors 갱신]
실전 코드 예제
1. Zod 스키마 정의 (Form + API DTO 일치)
// schemas/auth/signUp.ts
import { z } from 'zod';
export const signUpSchema = z.object({
email: z.string().email('유효한 이메일 형식'),
password: z.string().min(8, '비밀번호는 8자 이상 입력'),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
path: ['confirmPassword'],
message: '비밀번호가 일치하지 않습니다.',
});
}
});
export type SignUpRequest = z.infer<typeof signUpSchema>;
2. API 호출 함수
// api/auth.ts
import axios from '@/lib/axios';
import { SignUpRequest } from '@/schemas/auth/signUp';
export const signUpApi = (data: SignUpRequest) => {
return axios.post('/api/signup', data);
};
3. React Query mutation 훅
// hooks/useSignUpMutation.ts
import { useMutation } from '@tanstack/react-query';
import { signUpApi } from '@/api/auth';
export const useSignUpMutation = () => {
return useMutation({
mutationFn: signUpApi,
onSuccess: () => {
alert('회원가입 성공!');
},
onError: (error) => {
alert('회원가입 실패: ' + (error.response?.data?.message ?? '알 수 없는 오류'));
},
});
};
4. Form 컴포넌트 (RHF + zodResolver + UX 최적화)
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { signUpSchema, SignUpRequest } from '@/schemas/auth/signUp';
import { useSignUpMutation } from '@/hooks/useSignUpMutation';
export const SignUpForm = () => {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<SignUpRequest>({
resolver: zodResolver(signUpSchema),
mode: 'onBlur',
reValidateMode: 'onChange',
});
const mutation = useSignUpMutation();
const onSubmit = (data: SignUpRequest) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} placeholder="이메일" disabled={isSubmitting} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register('password')} placeholder="비밀번호" disabled={isSubmitting} />
{errors.password && <p>{errors.password.message}</p>}
<input type="password" {...register('confirmPassword')} placeholder="비밀번호 확인" disabled={isSubmitting} />
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '가입 중...' : '회원가입'}
</button>
</form>
);
};
| isSubmitting | submit 중 버튼 비활성화 |
| errors.xxx.message | 필드별 에러 표시 |
| mode: 'onBlur' | blur 시 검증 |
| reValidateMode: 'onChange' | 입력 중 실시간 재검증 |
| mutation.onError | API 에러 처리 UX (Toast, alert) |
| API 요청 DTO & Form 스키마 통합 | z.infer<typeof schema> |
전체 흐름
- 스키마 선언 (검증 로직 → Form + API 일치)
- useForm + zodResolver로 Form 상태 관리 + 검증
- onSubmit에서 React Query mutation 호출
- isSubmitting, onError, onSuccess → UX 최적화
- 코드는 깔끔, 상태 일관성 유지, 검증/에러/UI 분리
실전 프로젝트 폴더 구조 추천 (역할 분리)
src/
├── api/ # API 호출 함수 (순수 Axios wrapper, 비즈니스 로직 X)
│ ├── auth.ts
│ └── user.ts
│
├── hooks/ # React Query custom hooks (useUserQuery, useSignUpMutation)
│ ├── useSignUpMutation.ts
│ └── useUserQuery.ts
│
├── schemas/ # Zod 스키마 및 타입 관리 (Form + API DTO 통합)
│ ├── auth/
│ │ └── signUp.ts
│ │ └── login.ts
│ └── user/
│
├── components/ # UI 컴포넌트 (Form, Input, Button, etc)
│ ├── forms/
│ │ └── SignUpForm.tsx
│ └── ui/
│ └── Button.tsx
│
├── pages/ # 페이지 레벨 (Next.js 기준 or React Router 기준)
│ ├── SignUpPage.tsx
│ └── LoginPage.tsx
│
├── stores/ # Zustand 등 클라이언트 상태 관리
│ └── userStore.ts
│
├── lib/ # Axios 인스턴스, react-query client, 공통 error handler 등
│ ├── axios.ts
│ ├── react-query.ts
│ └── error-handler.ts
│
└── types/ # 공통 타입 (API 응답 타입, 유틸 타입)
└── api.ts
계층별 역할 & 책임
| api/ | 순수 API 호출 함수 (Axios wrapper) | UI, 비즈니스 로직 절대 X |
| hooks/ | React Query 상태 관리 (mutation, query) | 상태 & side effect 관리 |
| schemas/ | Zod 스키마 + 타입 (z.infer) 관리 | Form 검증 + API DTO 타입 통합 |
| components/ | UI 컴포넌트 (Form, 버튼, 입력 등) | Form은 검증 로직 없이 UI만 담당 |
| pages/ | 페이지 컴포넌트 (라우팅, SEO, 레이아웃) | RHF, Query, 상태 조합 |
| stores/ | 클라이언트 상태 (Zustand 등) | 서버 상태와 분리 |
| lib/ | Axios, QueryClient, 공통 유틸 | 에러 처리, 인터셉터, 공통 설정 |
| types/ | API 응답, 유틸 타입 관리 | 서버, 클라이언트 타입 일관화 |
실무 Best Practice 체크리스트
- API 레이어는 비즈니스 로직 X → Axios만 호출
- Form은 UI만 → 검증, 상태, mutation은 외부에서 관리
- Zod 스키마 → DTO 타입 통합 → API와 Form 검증 통일
- React Query → API 호출 + 캐싱 관리
- Zustand → 클라이언트 상태만 관리 (서버 상태와 구분)
- lib/에서 Axios, QueryClient 전역 관리 → 재사용성, 유지보수 향상
참고 흐름
[UI Input]
↓ (register)
[React Hook Form]
↓ (zodResolver)
[Zod 스키마 검증]
↓ (onSubmit)
[React Query Mutation → API 호출 (Axios)]
↓
[성공 → 상태 갱신, 알림]
[실패 → error-handler UX 처리]
onBlur, onChange 최적화 (mode, reValidateMode)
✔ mode 옵션 (최초 검증 시점 제어)
- RHF에서 초기 검증 시점을 제어하는 옵션
- Form이 로드되고 사용자가 처음으로 입력 필드를 조작했을 때, 언제 검증을 시작할지 결정
| 'onSubmit' (기본) | Form 제출 시 검증 |
| 'onBlur' | 필드에서 focus out 될 때 검증 |
| 'onChange' | 입력 중 실시간 검증 |
| 'all' | 입력, blur, submit 모두 검증 |
✔ reValidateMode 옵션 (검증 후 재검증 시점 제어)
- 한번 검증된 필드를 다시 검증할 시점
- 사용자가 에러가 있는 필드를 고칠 때, 언제 다시 검증할지 결정
reValidateMode 값 동작
| 'onChange' | 입력 시 다시 검증 (추천) |
| 'onBlur' | blur 시점에만 다시 검증 |
✔ 실무 추천 패턴
useForm({
mode: 'onBlur',
reValidateMode: 'onChange',
});
- UX 최적화 이유:
- 입력 필드 처음 포커스 아웃 시 검증 (mode: 'onBlur')
- 검증된 후 수정할 경우 즉시 실시간 검증 (reValidateMode: 'onChange')
- → 사용자가 불편하지 않게 빠르게 에러 확인 가능
- → 초반에는 느슨하게, 고칠 때는 적극적으로 검증
✔ 요약 기억
- mode: 최초 검증 타이밍
- reValidateMode: 검증된 필드를 다시 검증하는 타이밍
동적 필드 관리 (useFieldArray)
✔ 역할
- RHF에서 동적 필드 리스트(예: 태그, 참여자, 옵션 등)를 성능 저하 없이 관리
- 필드 추가/삭제 시 자동으로 register 연결, 검증, 상태 관리됨
- 복잡한 Form에서도 필드 추가/삭제 시 리렌더 최소화 → 고성능 유지
✔ 기본 사용 흐름
const { control, register, handleSubmit } = useForm({
defaultValues: {
skills: [{ name: '' }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'skills',
});
return (
<>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`skills.${index}.name`)} />
<button type="button" onClick={() => remove(index)}>삭제</button>
</div>
))}
<button type="button" onClick={() => append({ name: '' })}>추가</button>
</>
);
✔ 원리
- useFieldArray는 내부적으로 RHF의 control과 연결
- 필드 배열(fields)을 생성
- append, remove 사용 시 → 필드가 DOM에 동적으로 추가/삭제 → RHF가 알아서 상태 관리
- field.id가 필수 → 리렌더 최적화
실무요령
| 입력 UX 최적화 | mode: 'onBlur', reValidateMode: 'onChange' |
| 동적 리스트 관리 | useFieldArray + append, remove |
| 동적 필드 key 최적화 | field.id 사용 필수 |
6. RHF 상태 최적화 및 고급 사용법 (실무 기준)
1. ✔ 상태 최적화 (렌더링 최소화 & 비활성화 필드 관리)
RHF의 기본 렌더링 최적화
- RHF는 Input을 Uncontrolled 방식 + useRef 기반 관리
- → Input 상태 변경 시 Form 전체가 리렌더링되지 않고, 필요한 Input만 리렌더링
- → 불필요한 렌더링 없이 고성능 유지
shouldUnregister: true
- 조건부 렌더링되는 필드 제거 시 → 해당 필드의 값, 상태, 에러 자동으로 unregister
- 기본값은 false (숨겨진 필드라도 상태는 계속 유지)
- true 설정 시 → 비활성화되면 필드 상태, 에러 전부 제거 → Form 클린 상태 유지
const { register } = useForm({
shouldUnregister: true,
});
✔ Controller (외부 컴포넌트와 연결)
- register()는 기본 Input, Select, Textarea 같은 기본 HTML Input 요소만 직접 제어
- React-Select, DatePicker 같은 외부 라이브러리의 경우 → React Hook Form이 직접 value, onChange 관리 불가능
- → 이때 Controller 사용 필수 → RHF와 외부 컴포넌트 연결
import { Controller } from 'react-hook-form';
<Controller
control={control}
name="category"
render={({ field }) => (
<Select {...field} options={categoryOptions} />
)}
/>
2. ✔ 실시간 UI 반응형 Form (watch, setValue, reset)
watch()
- 필드 값을 실시간으로 구독
- Form이 리렌더되지 않아도 값 확인 가능
- UI Preview, 조건부 렌더링 시 사용
const watchedEmail = watch('email');
setValue()
- Form 내부 상태에서 값을 강제로 변경
- API 응답 데이터로 Form 값을 세팅하거나, 버튼 클릭 시 값 변경 가능
setValue('email', 'admin@example.com');
reset()
- Form 상태 전부 초기화 (필드 값, 에러, touched 등)
reset({
email: '',
password: '',
});
3. ✔ 에러 UX 최적화
errors map으로 UX 표시
{Object.keys(errors).map((fieldName) => (
<p key={fieldName}>{errors[fieldName]?.message}</p>
))}
formState.isValid
- 전체 Form이 유효한지 확인 가능
- mode: 'onChange' 또는 onBlur 설정 시 실시간 반영됨
- → submit 버튼 비활성화/활성화 제어에 유용
<button type="submit" disabled={!formState.isValid}>
제출
</button>
실무 UX 최적화 핵심 요령 요약
| 불필요 렌더 최소화 | 기본 Uncontrolled Input + shouldUnregister: true | ✅ |
| 외부 컴포넌트 연결 | Controller | ✅ |
| 실시간 상태 UI | watch() | ✅ |
| Form 값 변경 | setValue(), reset() | ✅ |
| 에러 UX 최적화 | errors map, formState.isValid | ✅ |
7. 실전 최종 예제
상황:
사용자는 관심 태그 추가/삭제가 가능한 회원가입 Form을 입력
- React Hook Form → Form 상태, 에러, 최적화
- Zod → 스키마 검증 + API DTO 통합
- React Query → API 호출 (mutation)
- useFieldArray → 동적 태그 추가/삭제
- Controller → 외부 라이브러리(Select) 사용
- isSubmitting, isValid → UX 최적화
1. Zod 스키마 정의
// schemas/auth/signUp.ts
import { z } from 'zod';
export const signUpSchema = z.object({
email: z.string().email('유효한 이메일'),
password: z.string().min(8, '8자 이상 입력'),
confirmPassword: z.string(),
tags: z.array(z.string().min(2, '2글자 이상 태그')),
category: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
path: ['confirmPassword'],
message: '비밀번호가 일치하지 않습니다.',
});
}
});
export type SignUpRequest = z.infer<typeof signUpSchema>;
2. API 호출 (React Query)
// api/auth.ts
import axios from '@/lib/axios';
import { SignUpRequest } from '@/schemas/auth/signUp';
export const signUpApi = (data: SignUpRequest) => {
return axios.post('/api/signup', data);
};
3. Form 컴포넌트
import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { signUpSchema, SignUpRequest } from '@/schemas/auth/signUp';
import { signUpApi } from '@/api/auth';
import Select from 'react-select'; // 외부 Select 라이브러리
const categoryOptions = [
{ label: '개발', value: 'dev' },
{ label: '디자인', value: 'design' },
];
export const SignUpForm = () => {
const { register, handleSubmit, control, watch, setValue, reset, formState: { errors, isSubmitting, isValid } } = useForm<SignUpRequest>({
resolver: zodResolver(signUpSchema),
mode: 'onBlur',
reValidateMode: 'onChange',
defaultValues: {
tags: [{ value: '' }],
category: '',
},
shouldUnregister: true,
});
const { fields, append, remove } = useFieldArray({
control,
name: 'tags',
});
const mutation = useMutation({
mutationFn: signUpApi,
onSuccess: () => {
alert('회원가입 성공');
reset();
},
onError: (error) => {
alert('회원가입 실패: ' + (error.response?.data?.message ?? '알 수 없는 오류'));
},
});
const onSubmit = (data: SignUpRequest) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} placeholder="이메일" disabled={isSubmitting} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register('password')} placeholder="비밀번호" disabled={isSubmitting} />
{errors.password && <p>{errors.password.message}</p>}
<input type="password" {...register('confirmPassword')} placeholder="비밀번호 확인" disabled={isSubmitting} />
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
{/* 동적 태그 관리 */}
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`tags.${index}`)} placeholder="태그" disabled={isSubmitting} />
<button type="button" onClick={() => remove(index)}>삭제</button>
{errors.tags?.[index] && <p>{errors.tags[index]?.message}</p>}
</div>
))}
<button type="button" onClick={() => append('')}>태그 추가</button>
{/* 외부 Select (Controller 사용) */}
<Controller
name="category"
control={control}
render={({ field }) => (
<Select
{...field}
options={categoryOptions}
isDisabled={isSubmitting}
/>
)}
/>
{errors.category && <p>{errors.category.message}</p>}
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? '가입 중...' : '회원가입'}
</button>
</form>
);
};
최적화 포인트
| Zod 스키마로 검증 + API DTO 통일 | ✅ |
| React Query mutation으로 API 호출 + 상태 관리 | ✅ |
| useFieldArray로 동적 태그 추가/삭제 | ✅ |
| Controller로 외부 Select 라이브러리 연결 | ✅ |
| watch, setValue, reset으로 상태 제어 및 실시간 Preview 가능 | ✅ |
| isSubmitting → submit 중 UI 잠금 | ✅ |
| isValid → submit 버튼 활성화 UX 제어 | ✅ |
'JavaScript > React' 카테고리의 다른 글
| React-Spring (3) | 2025.05.23 |
|---|---|
| dnd-kit(Drag-and-Drop 라이브러리) (0) | 2025.05.20 |
| React Query (1) | 2025.05.15 |
| Zustand(persist, Zukeeper, Redux DevTools 등) (0) | 2025.04.02 |
| React에서 화면 사이즈 감지하기(반응형 설정하기, window.innerWidth, matchMedia, react-responsive, tailwind) (0) | 2025.03.31 |