제어 컴포넌트는 값이 React state에 있는 폼 요소입니다. value를 넘기고 onChange에서 state를 업데이트하므로, input이 무엇을 보여 줄지 React가 결정합니다. 비제어 컴포넌트는 일반 HTML처럼 값을 DOM에 보관합니다. 시작 값인 defaultValue를 넘기고, 나중에 ref나 FormData로 현재 값을 읽습니다.
두 input에 모두 입력해 보세요. 문단은 키를 누를 때마다 첫 번째 input을 따라갑니다. 변경될 때마다 state를 설정하고 다시 렌더링하기 때문입니다. 두 번째 input은 DOM에서만 바뀝니다. React는 렌더링하지 않으며, "Read both"가 값을 요청할 때만 그 값을 볼 수 있습니다.
제어 input
제어 input에서는 React state가 유일한 진실의 원천입니다. 키를 누를 때마다의 흐름은 다음과 같습니다.
- 사용자가 입력하고, 브라우저가 input 이벤트를 발생시킵니다.
onChange핸들러가e.target.value를 읽고 setter를 호출합니다.- React가 다시 렌더링하고 새 state를 input의
value에 다시 씁니다.
코드가 그 반복의 한가운데에 있기 때문에 비제어 input으로는 할 수 없는 일을 할 수 있습니다. 문자를 거부하거나 바꾸기, 값을 다른 곳에 보여 주기, 값이 유효해질 때까지 버튼 비활성화하기, 코드에서 값 설정하기("Clear" 버튼, 사용자가 클릭하는 추천 항목) 같은 것들입니다.
글자를 입력해 보세요. 핸들러가 state를 설정하기 전에 숫자가 아닌 것을 모두 걸러 내므로 글자는 절대 나타나지 않습니다. Verify 버튼은 정확히 여섯 자리 숫자가 될 때까지 비활성화된 상태로 남습니다.
폼 페이지의 예제들이 사용하는 것이 제어 방식이며, 대부분의 React 코드에서 기본 선택입니다.
비제어 input
비제어 input은 일반 HTML처럼 동작합니다. React는 defaultValue(체크박스와 라디오는 defaultChecked)로 한 번 렌더링한 뒤 그대로 둡니다. 값을 읽으려면 요소에 대한 ref를 보관하거나, 제출할 때 FormData로 폼의 이름 있는 필드를 모두 읽습니다.
필드를 위한 state도 onChange도 없습니다. 값은 폼이 제출될 때만 수집됩니다. 체크박스를 해제하고 다시 보내 보세요. 체크되지 않은 체크박스는 FormData에 아예 포함되지 않으므로 subscribe 키가 사라집니다.
React는 요소가 만들어질 때만 defaultValue를 적용하므로, 첫 렌더링 후에 defaultValue를 바꿔도 input에는 아무 변화가 없습니다. 비제어 폼을 초기화하려면 e.target.reset()을 호출하거나, 폼에 새 key를 주어 React가 새 input을 만들게 하세요.
입력할 수 없는 input
value는 넘기고 onChange는 넘기지 않으면 input은 제어되지만, state를 업데이트하는 것은 아무것도 없습니다. React가 키를 누를 때마다 같은 값을 다시 쓰므로 input이 얼어붙은 것처럼 보입니다.
각 필드에 입력해 보세요. 첫 번째 필드는 바뀌지 않습니다. 두 번째 필드는 제어 input을 올바르게 고친 것이며, 거기에 입력하면 세 필드가 모두 같은 state를 읽기 때문에 첫 번째와 마지막 필드가 따라 바뀝니다. 세 번째 필드는 비제어이므로 입력을 받습니다. 네 번째 필드는 편집할 수 없다고 일부러 밝힌 것입니다.
개발 환경에서 React는 첫 번째 input에 대해 "You provided a value prop to a form field without an onChange handler"라는 경고를 기록합니다. 미리보기는 프로덕션 빌드처럼 실행되므로 여기서는 그 경고가 보이지 않지만, 직접 띄운 개발 서버에서는 보입니다.
비제어에서 제어로 바뀌는 경우
React는 value를 보고 input이 제어되는지 판단합니다. value가 undefined나 null이면 input은 비제어입니다. 그래서 value가 undefined로 시작했다가 나중에 문자열이 되는 input은 살아 있는 동안 비제어에서 제어로 바뀌며, React는 개발 환경에서 "A component is changing an uncontrolled input to be controlled"라고 경고합니다.
이 경고는 실제 버그를 가리킵니다. 반대로 문자열에서 undefined로 돌아가면 input은 다시 비제어가 되고, React는 더 이상 그 input에 값을 쓰지 않습니다.
두 input에 무언가를 입력하고 각 Clear 버튼을 눌러 보세요. 두 번째 input은 비워집니다. 첫 번째 input은 텍스트를 유지합니다. state를 undefined로 설정하면서 비제어가 되었으므로, React가 더 이상 무엇을 보여 줄지 제어하지 않기 때문입니다.
undefined가 생기는 흔한 원인은 아직 불러오지 않은 데이터(사용자 정보가 오기 전의 value={user.name})나 아직 존재하지 않는 객체 필드입니다. 해결 방법은 두 가지입니다.
const [name, setName] = useState(''); // start with a string
<input value={user?.name ?? ''} /> // or fall back to one
체크박스에서는 같은 규칙이 checked에 적용됩니다. undefined가 아니라 false로 시작하세요.
파일 input은 항상 비제어입니다
<input type="file">은 제어할 수 없습니다. 브라우저가 페이지에서 그 값을 설정하지 못하게 하기 때문입니다(디스크의 파일을 고를 수 있는 페이지는 보안 구멍이 될 것입니다). 선택한 파일은 onChange에서 e.target.files로 읽거나, 폼을 제출할 때 ref로 읽으세요.
컴퓨터에서 파일을 한두 개 골라 보세요. 아무것도 업로드되지 않으며, 예제는 이름과 크기만 나열합니다. 파일 input에 설정할 수 있는 유일한 값은 빈 문자열이며, Clear 버튼이 그렇게 초기화합니다.
언제 무엇을 고를까
| 제어 | 비제어 | |
|---|---|---|
| 값이 있는 곳 | React state | DOM |
| 시작 값 설정 | value / checked | defaultValue / defaultChecked |
| 값 읽기 | 언제든 state에서 | ref로, 또는 제출 시 FormData로 |
| 키 입력마다 렌더링 | 예 | 아니요 |
| 코드에서 값 바꾸기 | state 설정 | ref를 통해 DOM에 쓰기 |
| 적합한 경우 | 실시간 유효성 검사, 포맷팅, 의존하는 필드 | 제출 시 읽는 간단한 폼, 파일 input, 폼 액션 |
사용자가 입력하는 동안 화면이 값에 반응해야 한다면 제어를 고르세요. 마지막에만 값이 필요하다면 비제어를 고르세요. React 19 폼 액션도 이렇게 동작합니다. <form action={fn}>은 함수에 FormData를 넘기고 그 뒤에 비제어 필드를 초기화합니다.
키를 누를 때마다 렌더링한다고 하면 비용이 커 보이지만, 일반적인 폼에서는 문제가 되지 않습니다. 크고 느린 트리가 input과 함께 다시 렌더링될 때만 문제가 됩니다. 그럴 때는 state를 input 가까이에 두거나, 느린 부분을 별도의 컴포넌트로 옮기세요.
같은 두 단어는 직접 만든 컴포넌트에도 쓰입니다. 부모에게서 props로 값과 onChange를 받는 컴포넌트는 부모가 제어하는 것이고, 자기 state를 가진 컴포넌트는 비제어입니다. 컴포넌트를 제어 컴포넌트로 만들기 위해 state를 위로 옮기는 것이 상태 끌어올리기의 내용입니다.
자주 묻는 질문
React에서 제어 컴포넌트란 무엇인가요?
현재 값이 React state에서 오는 폼 요소입니다. value(또는 checked)로 값을 설정하고 onChange로 업데이트합니다. React state가 유일한 진실의 원천이므로 항상 값을 알 수 있고 코드에서 바꿀 수 있습니다.
React에서 비제어 컴포넌트란 무엇인가요?
일반 HTML처럼 자기 값을 DOM에 보관하는 폼 요소입니다. defaultValue(또는 defaultChecked)로 시작 값을 주고, 필요할 때 ref나 제출 시의 FormData로 현재 값을 읽습니다.
React input에 입력이 안 되는 이유는 무엇인가요?
onChange 없이 value를 넘겼기 때문입니다. input이 제어되고 있으므로 React가 키를 누를 때마다 state 값을 다시 넣습니다. state를 업데이트하는 onChange를 추가하거나, defaultValue로 바꾸거나, 정말 바뀌면 안 되는 값이라면 readOnly를 추가하세요.
"A component is changing an uncontrolled input to be controlled" 경고는 어떻게 고치나요?
input의 value가 undefined(또는 null)로 시작했다가 나중에 문자열이 되었기 때문입니다. 항상 문자열이 되도록 하세요. state를 ''로 시작하거나 value={name ?? ''}로 쓰세요.
제어 input과 비제어 input 중 무엇을 써야 하나요?
사용자가 입력하는 동안 UI가 값에 반응해야 한다면 제어 input을 쓰세요. 유효성 검사, 포맷팅, 비활성화 버튼, 서로 의존하는 필드가 그런 경우입니다. 제출할 때만 값이 필요하다면 비제어 input을 쓰고, 파일 input은 항상 비제어입니다.