📌 들어가며
React의 폼 입력 관리 방식인 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)를 비교하고, 비제어 방식의 단점을 React Hook Form이 어떻게 보완하는지, 그리고 Zod와 함께 사용하면 좋은 이유를 정리한다.
📌 제어 컴포넌트 (Controlled Component)
React의 state가 입력값을 직접 제어하는 방식
function ControlledInput() {
const [value, setValue] = useState("");
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
- 입력값(value)이 항상 state에 의해 결정됨
- React 렌더링마다 값이 갱신되어 예측 가능하고 디버깅이 쉬움
- 대신 모든 입력 변경마다 리렌더링이 발생해 성능 부담이 커질 수 있음
📌 비제어 컴포넌트 (Uncontrolled Component)
입력값을 React가 아닌 DOM이 직접 관리하는 방식
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => alert(inputRef.current.value);
return (
<form>
<input type="text" ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
- value 대신 defaultValue를 사용
- 값 변경 시 React가 렌더링을 트리거하지 않음
- 간단하고 빠르지만, 다음과 같은 단점이 있음 👇
⚠️ 비제어 컴포넌트의 단점
- 값 추적 어려움 → ref.current.value로 직접 접근해야 함
- 유효성 검사 불편 → 입력 이벤트마다 수동 검사 필요
- 초기값 변경 반영 안 됨 → defaultValue는 첫 렌더링에만 적용됨
- React 데이터 흐름 위배 → 단방향 데이터 플로우가 깨짐
- 테스트 어려움 → DOM 접근 기반 로직이라 테스트 불편
📌 비제어의 단점을 보완한 폼 라이브러리 (React Hook Form)
RHF는 비제어 컴포넌트 기반이지만, 내부적으로 ref와 Proxy를 조합해 React스럽게 제어 가능한 상태를 만든다.
💡 핵심 아이디어
- 각 필드를 register()로 등록해 내부적으로 ref를 추적
- 입력값이 변경되어도 React 전체가 리렌더링되지 않음
- 제출 시점에 getValues()로 모든 필드 값 수집
- errors, formState 등으로 폼 상태를 선언적으로 관리
import { useForm } from 'react-hook-form';
function Form() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: '이메일은 필수입니다' })} />
{errors.email && <p>{errors.email.message}</p>} <button>제출</button>
</form>
);
}
✅ 비제어 컴포넌트의 단점을 RHF가 해결한 방식
- 값 추적 어려움 → register, getValues, watch로 접근
- 유효성 검사 어려움 → rules로 선언적 검증
- 초기값 변경 불가 → defaultValues로 선언적 초기화
- 리렌더링 과다 → ref 기반 관리로 성능 향상
🔍 Zod와 함께 쓰면 좋은 이유
RHF는 기본적으로 단순한 rule 기반 검증을 제공하지만, 복잡한 도메인 로직이나 다단계 스키마 검증이 필요한 경우 Zod를 결합하면 훨씬 강력해질 수 있다
✅ 이유 요약
- 타입 안전성 보장 → TypeScript와 궁합이 뛰어남
- RHF와 완벽한 통합 → @hookform/resolvers/zod로 손쉽게 연결 가능
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email('유효한 이메일을 입력하세요'),
password: z.string().min(6, '비밀번호는 6자 이상이어야 합니다'),
});
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ resolver: zodResolver(schema) });
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<input {...register('password')} type="password" />
{errors.password && <p>{errors.password.message}</p>}
<button>제출</button>
</form>
);
}
📚 오늘의 배움
- React Hook Form은 비제어 방식의 효율성과 제어 방식의 일관성을 모두 취한 하이브리드 접근이다.
- RHF와 Zod를 결합하면 타입 안정성과 선언적 검증까지 확보되어, 다양한 타입의 폼을 효율적이고 안정적으로 관리할 수 있는 실용적인 조합이 된다.
'FE > React' 카테고리의 다른 글
| [TIL] 나는 React Query를 왜 사용했을까? (0) | 2025.11.14 |
|---|---|
| [React 까보기] 5. setState (feat. dispatchAction) (0) | 2025.06.11 |
| [React 까보기] 4. useState 의 구현체 (0) | 2025.06.11 |
| [React 까보기] 3. Hook 구현체 찾아가기 (0) | 2025.04.26 |
| [React 까보기] 2. VDOM 과 React lifecycle (0) | 2025.04.14 |