노트

제어·비제어 컴포넌트

Controlled and Uncontrolled Components

프론트엔드#react · 연결된 개념 2개

쉽게 말하면

제어 컴포넌트는 입력 칸에 글자를 칠 때마다 React가 장부에 옮겨 적는 방식이고, 비제어 컴포넌트는 칸에 그냥 두었다가 제출할 때 한 번 꺼내 보는 방식이에요. 실시간 검사가 필요하면 앞쪽, 칸이 많으면 뒤쪽이 편해요.

비유의 한계 한 폼 안에서 둘을 섞으면 값이 어디 있는지 헷갈려요. 한 입력이 제어에서 비제어로 바뀌면 React가 경고하고, react-hook-form은 비제어로 렌더를 줄이면서 필요한 필드만 제어처럼 써요.

제어 컴포넌트(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} />
제어비제어
진실 원천stateDOM
렌더입력마다거의 없음
실시간 검증·포맷쉬움따로 처리
코드량많음적음, 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"

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • Vue 3와 React 대응표

    React에 익숙한 개발자가 Vue 3(Composition API, script setup)를 빠르게 읽을 수 있게 짝지어 본 표. 가장 큰 차이는 Vue는 반응성(Reactivity)을 자동으로 추적해서 의존성 배열(Dependency Array)이 없다는 점이다.

  • 내부 가변성과 메모이제이션

    내부 가변성(Interior Mutability)은 객체나 함수의 참조는 그대로인데 안에 숨은 상태가 바뀌는 성질이다. React의 메모이제이션은 "입력이 다른 참조인가"만 보기 때문에, 참조가 같으면 내용이 바뀌어도 바뀌지 않았다고 판단한다. React 공식 문서는 이를 "겉모습은 같은데 몰래 내용물을 바꾸는 상자"에 비유한다.

  • React로 보는 SOLID

    SOLID는 원래 객체 지향 설계 원칙이지만 React 컴포넌트·props·훅에도 그대로 옮겨 볼 수 있다. 원칙 자체보다 "어떤 코드 모양이 되는가"를 짧게 정리한다.

  • Redux 이후의 상태 관리 선택

    한때 React 앱의 기본값이던 Redux는 2025~2026년 기준으로 여러 선택지 중 하나가 됐다. 상태를 종류별로 나눠 각자에 맞는 도구를 고르는 방식이 자리 잡았기 때문이다.

  • Effect가 필요 없는 경우

    Effect는 외부 시스템과 동기화할 때 쓰는 탈출구(Escape Hatch)다. 렌더 중에 계산할 수 있는 값이나 사용자 이벤트 처리에 Effect를 쓰면 렌더가 한 번 더 일어나고 흐름이 꼬인다. React 공식 문서가 이런 경우를 따로 정리해 두었다.

보기 옵션