TL;DR: 내부 상태(
state)로 스스로 동작하면 비제어(Uncontrolled), 부모가props로 모든 핵심 정보를 내려주면 제어(Controlled) 컴포넌트
React 공식 문서에서는 다음과 같이 설명하고있다.
It is common to call a component with some local state “uncontrolled”. For example, the original Panel component with an isActive state variable is uncontrolled because its parent cannot influence whether the panel is active or not.
In contrast, you might say a component is “controlled” when the important information in it is driven by props rather than its own local state. This lets the parent component fully specify its behavior. The final Panel component with the isActive prop is controlled by the Accordion component.
Uncontrolled components are easier to use within their parents because they require less configuration. But they’re less flexible when you want to coordinate them together. Controlled components are maximally flexible, but they require the parent components to fully configure them with props.
In practice, “controlled” and “uncontrolled” aren’t strict technical terms—each component usually has some mix of both local state and props. However, this is a useful way to talk about how components are designed and what capabilities they offer.
When writing a component, consider which information in it should be controlled (via props), and which information should be uncontrolled (via state). But you can always change your mind and refactor later.
(의역):
지역 상태(state)를 가진 컴포넌트는 일반적으로 비제어(uncontrolled) 컴포넌트라 부른다. 예를 들어 isActive 라는 상태를 내부에 두고 있는 초기 Panel 컴포넌트는 부모가 활성/비활성 여부를 조정할 수 없으므로 비제어다.
반대로, 중요한 정보가 로컬 state가 아니라 props로 전달될 때 이를 제어(controlled) 컴포넌트라 한다. 이렇게 하면 부모가 컴포넌트의 동작을 완전히 정의할 수 있다. isActive prop을 받는 최종 Panel 컴포넌트가 Accordion 컴포넌트에 의해 제어되는 것이 그 예다.
비제어 컴포넌트는 설정할 것이 적어 쓰기 간편하지만, 여러 컴포넌트를 함께 조정하기엔 유연성이 낮다. 제어 컴포넌트는 최대한의 유연성을 제공하지만, 부모가 세세히 관리해야 한다는 부담이 있다.
실제로 대부분의 컴포넌트는 state와 props를 적절히 섞어 쓴다. 어떤 정보를 제어할지/비제어로 둘지 고민하고, 필요하다면 언제든 리팩터링하자.
| 구분 | 비제어(Uncontrolled) | 제어(Controlled) |
|---|---|---|
| 데이터 소스 | 컴포넌트 내부 useState | 부모 props |
| 사용 난이도 | 간단 (빠른 프로토타입) | 복잡 (설정 필요) |
| 재사용·조율 | 제한적 (상태 분리 어려움) | 매우 유연 (부모에서 완전 제어) |
| 대표 사례 | Form 입력을 ref로 직접 제어 | Form 입력을 value/onChange로 제어 |
| 장점 | 설정 적음, 코드 짧음 | 상태 단일 source of truth 유지, 테스트 용이 |
| 단점 | 상태 분산, 예측 어려움 | 보일러플레이트 증가 |
📝 팁 :
작은 위젯·독립 모듈 → 비제어
대규모 폼·여러 컴포넌트가 동일 상태를 공유 → 제어 패턴이 적합
// 부모가 알 필요 없는 간단 토글 패널
function Panel({ title, children }) {
const [isActive, setIsActive] = React.useState(false);
return (
<section>
<h3 onClick={() => setIsActive(!isActive)}>{title}</h3>
{isActive && <div>{children}</div>}
</section>
);
}
function Panel({ title, isActive, onToggle, children }) {
return (
<section>
<h3 onClick={onToggle}>{title}</h3>
{isActive && <div>{children}</div>}
</section>
);
}
function Accordion({ items }) {
const [activeIndex, setActiveIndex] = React.useState(null);
return (
<div>
{items.map((item, idx) => (
<Panel
key={idx}
title={item.title}
isActive={activeIndex === idx}
onToggle={() =>
setActiveIndex(activeIndex === idx ? null : idx)
}
>
{item.content}
</Panel>
))}
</div>
);
}
Accordion 이 모든 Panel 의 열림/닫힘을 통제. 복잡한 UX도 쉽게 구현.Panel 사용 시 반드시 isActive 와 onToggle 을 내려줘야 한다.// 비제어 input (ref 이용)
function UncontrolledInput() {
const inputRef = React.useRef();
function handleSubmit(e) {
e.preventDefault();
alert(inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button>Submit</button>
</form>
);
}
// 제어 input (state 이용)
function ControlledInput() {
const [value, setValue] = React.useState("");
function handleChange(e) {
setValue(e.target.value);
}
return (
<input type="text" value={value} onChange={handleChange} />
);
}
value 를 직접 읽으므로 구현이 빠르지만, 값 변화를 실시간으로 추적하기 어렵다.| 상황 | 추천 방식 |
|---|---|
| 독립적인 위젯 · 빠른 프로토타입 | 비제어 |
| 많은 입력 필드가 서로 의존 · 실시간 검증 | 제어 |
| 부모에서 다양한 조합/조건부 렌더링 필요 | 제어 |
| DOM API(파일 업로드, 비디오 등)와 직접 상호작용 | 비제어 또는 hybrid |
✅ 결론 : 고민되면 우선 비제어로 시작 → 복잡해지면 제어로 리팩터링!