React의 제어 컴포넌트 vs 비제어 컴포넌트
📘 Official Docs
- React - Controlled Components: React state로 input 값을 직접 관리하는 방식
- React - Uncontrolled Components: DOM이 자체적으로 값 관리하고 ref로 접근하는 방식
- React Hook Form - Controller
제어 컴포넌트 (Controlled Component)
입력값을 React state가 직접 관리하는 컴포넌트를 제어 컴포넌트라고 한다.value와 onChange를 통해 관리한다.
예시
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}- input의 실제 값은 value state
- DOM은 단순히 React가 내려준 값을 렌더링한다.
즉, 값의 주도권은 항상 React에 있다.
장점
- 값에 즉시 접근 가능
- 실시간 검증 쉬움
- React가 전체 흐름을 제어
단점
- 입력할 때마다 리렌더링 발생
- 필드가 많아질수록 state와 핸들러 증가
- 폼이 커질수록 코드 복잡도 증가
단, 실시간 검증이나 조건부 UI가 핵심인 경우에는
이러한 비용을 감수하고서라도 제어 컴포넌트를 사용하는 것이 자연스럽다.
비제어 컴포넌트 (Uncontrolled Component)
입력값을 DOM이 직접 관리하고, React는 ref로 필요할 때만 값을 가져오는 방식을 비제어 컴포넌트라고 한다.
예시
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return (
<>
<input ref={inputRef} />
<button onClick={handleSubmit}>확인</button>
</>
);
}- input 값은 DOM이 관리
- React는 필요할 때만 값을 조회
즉, 값의 주도권은 DOM에 있다.
장점
- 입력 중 리렌더링 없음
- 불필요한 state 제거 가능
- 대규모 폼에서 성능상 유리
단점
- 값의 흐름이 눈에 보이지 않음
- 실시간 검증, 조건부 UI에 불리
- 단순 구현은 쉽지만, 검증, 에러 처리, 제출 흐름이 분산되기 쉬워 구조화가 어렵다.
React Hook Form에서의 제어 / 비제어
React Hook Form은 비제어 컴포넌트 방식을 기반으로 설계되었다. 하지만 단순히 ref만 쓰는 라이브러리는 아니다.
비제어 컴포넌트 + 폼 상태 관리 + 유효성 검증 + 제출 흐름 제어- 즉, DOM이 입력값을 관리하고 React Hook Form이 그 값을 수집·검증·관리한다.
register
<input {...register('email')} />register는 내부적으로 ref, onChange, onBlur, name를 input에 주입한다.
(DOM 참조, 값 변경 감지, 포커스 해제 감지, 필드 이름)
DOM은 그대로 값을 관리하지만, React Hook Form은 ref와 이벤트를 통해 값의 변경 사실만 추적한다.
왜 controlled + register를 섞으면 안 될까?
// 잘못된 예
<input {...register('email')} value={email} />위 코드는 DOM에게도 값을 맡기고, React state에게도 값을 맡기는 구조다.
즉, 값의 주도권이 두 군데로 갈라진다.
이 상태에서는 React와 DOM 중 누가 실제 값을 책임지는지 알 수 없게 된다. 그래서 RHF는 이 사용을 금지한다.
그럼 제어 컴포넌트가 필요한 경우는?
- 외부 UI 라이브러리 (MUI, Ant Design)
- 내부적으로 value/onChange를 강제하는 컴포넌트
이때 사용하는 것이 Controller다.
<Controller
name='email'
control={control}
render={({ field }) => <CustomInput {...field} />}
/>Controller는 내부적으로 제어 컴포넌트를 사용하면서 React Hook Form과 안전하게 연결해준다.
즉, Controller는 제어 컴포넌트를 쓰지 않을 수 없는 상황에서
React Hook Form의 규칙을 깨지 않도록 도와주는 어댑터다. (내부적으로는 RHF가 제어 컴포넌트를 비제어 방식처럼 다룰 수 있게 감싸준다)
정리
- 제어 컴포넌트: React state가 값을 관리 → 실시간 제어 가능, 리렌더링 발생
- 비제어 컴포넌트: DOM이 값을 관리 → 성능 우수, 실시간 제어 어려움
- React Hook Form: 비제어 컴포넌트를 기반으로 폼 상태와 검증 흐름을 관리
- Controller: 제어 컴포넌트와 React Hook Form을 연결하기 위한 공식 API