제어 컴포넌트(Controlled Component)는 입력값의 진실 원천(Single Source of Truth)이 React state이고, 비제어 컴포넌트(Uncontrolled Component)는 입력값을 DOM(Document Object Model)이 직접 들고 있어서 필요할 때 ref로 꺼내 읽는다.
// 제어: 키 입력마다 state → 렌더
const [email, setEmail] = useState('')
<input value={email} onChange={e => setEmail(e.target.value)} />
// 비제어: DOM이 보관, 제출 시 읽기
const ref = useRef(null)
<input defaultValue="" ref={ref} />| 제어 | 비제어 | |
|---|---|---|
| 진실 원천 | state | DOM |
| 렌더 | 입력마다 | 거의 없음 |
| 실시간 검증·포맷 | 쉬움 | 따로 처리 |
| 코드량 | 많음 | 적음, HTML(HyperText Markup Language) 폼과 잘 맞음 |
- 제어 방식은 React의 단방향 데이터 흐름(One-Way Data Flow)에 맞아 예측하기 쉽다. 입력 중 검증, 마스킹, 다른 필드에 따른 조건부 렌더가 필요하면 이쪽이다(React 상태 갱신)
- 비제어 방식은 리렌더가 적다. 필드가 많은 폼에서 성능이 낫다
- react-hook-form이 비제어 기반인 이유도 이것이다. 입력할 때마다 렌더하지 않고, 필드 초기값을 state로 들 필요가 없으며, 네이티브 폼 동작을 활용한다. 필요하면 일부 필드만 제어 방식처럼 쓸 수 있다
- 같은 폼 안에서 둘을 섞으면 값이 어디에 있는지 헷갈린다. 하나의 컴포넌트가 제어였다가 비제어로 바뀌면 React가 경고한다
- 서버로 바로 제출하는 폼은 form action과 비제어 입력이 자연스럽게 맞는다
흔한 함정
- undefined에서 문자열로:
value={user?.name}은 데이터가 오기 전엔undefined라 비제어로 시작했다가 값이 오면 제어로 바뀐다. React는 입력이 도중에 비제어에서 제어로 바뀌었다고 경고한다. 제어 입력에는 처음부터 문자열을 준다(value={user?.name ?? ''}) - onChange 없는 value:
value만 주고onChange를 빠뜨리면 타이핑해도 바뀌지 않는 읽기 전용 입력이 되고 콘솔에 경고가 뜬다. 초기값만 줄 거면defaultValue, 일부러 막는 거면readOnly를 쓴다 type="file": 보안상 스크립트로 파일 입력의 값을 정할 수 없다. 그래서 항상 비제어로 두고, 선택된 파일은 ref나 이벤트의files로 읽는다
비제어 입력은 ref 대신 FormData로 한꺼번에 읽을 수도 있다. 입력마다 name을 붙이면 그 이름이 키가 된다.
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const data = Object.fromEntries(formData.entries()) // { email: '...' }
}
// <form onSubmit={handleSubmit}><input name="email" /></form>출처: React — input: 비제어→제어 경고 · React — 입력이 업데이트되지 않을 때 · React — 제출할 때 값 읽기 · MDN — input type="file"