상태(state)란 무엇인가
React에서 state의 개념과 props와의 차이, state 변경 시 리렌더링이 발생하는 원리, 그리고 state가 스냅샷처럼 동작한다는 핵심 개념을 설명합니다.
지난 글에서 사용자 이벤트를 처리하는 방법을 살펴봤습니다. 이번에는 React에서 UI를 동적으로 만드는 핵심 메커니즘인 state(상태)가 무엇인지, 어떻게 동작하는지 깊이 이해해 봅니다.
state란
state는 컴포넌트가 시간이 지나면서 기억해야 하는 데이터입니다. 사용자가 버튼을 몇 번 클릭했는지, 어떤 탭이 선택되었는지, 모달이 열려 있는지 — 이런 정보들이 state입니다.
일반 JavaScript 변수와 달리 state는 두 가지 특별한 능력을 가집니다.
- 렌더링 간 유지: 함수가 끝나도 state 값이 사라지지 않습니다.
- 변경 시 리렌더링 트리거: state가 바뀌면 React가 컴포넌트를 다시 실행해 최신 UI를 화면에 반영합니다.
// 일반 변수 — 리렌더 시마다 초기화됨, 변경해도 화면 안 바뀜
let count = 0;
// state — 렌더 간 유지 + 변경 시 자동 리렌더
const [count, setCount] = useState(0);
state vs props
| 특성 | props | state |
|---|---|---|
| 출처 | 부모 컴포넌트 | 컴포넌트 자체 |
| 읽기/쓰기 | 읽기 전용 | setXxx()로 변경 |
| 목적 | 컴포넌트 설정 | 내부 동적 데이터 |
| 변경 주체 | 부모 | 컴포넌트 자신 |
props는 “외부에서 설정하는 것”, state는 “컴포넌트가 스스로 관리하는 기억”으로 이해하면 됩니다.
state를 써야 할 때
모든 데이터가 state일 필요는 없습니다. 다음 질문으로 판단합니다.
- 시간이 지나도 변하지 않는가? → 변수나 상수로 충분
- 부모가 내려준 데이터인가? → props 사용
- 다른 state나 props로 계산 가능한가? → 파생 값으로 계산
function ShoppingCart({ cartItems }) {
// ✅ state: 사용자가 선택한 쿠폰 코드
const [couponCode, setCouponCode] = useState('');
// ✅ 파생 값: state/props로 계산 — 별도 state 불필요
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
const discountedPrice = couponCode === 'SAVE10'
? totalPrice * 0.9
: totalPrice;
return (
<div>
<p>합계: {discountedPrice.toLocaleString()}원</p>
<input value={couponCode} onChange={e => setCouponCode(e.target.value)} />
</div>
);
}
state는 스냅샷이다
React state를 이해하는 가장 중요한 개념은 “state는 렌더링 시점의 스냅샷”이라는 것입니다.
// 한 렌더 안에서 state는 변하지 않음
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // count = 0 → 예약: 1
setCount(count + 1); // count = 0 → 예약: 1 (여전히 0!)
setCount(count + 1); // count = 0 → 예약: 1
// 세 번 호출해도 결과: count = 1 (0+1)
}
return <button onClick={handleClick}>{count}</button>;
}
동일한 렌더 안에서 count는 항상 0입니다. setCount(count + 1)를 아무리 여러 번 호출해도 0 + 1 = 1로 계산됩니다.
이전 state를 기반으로 업데이트하려면 업데이터 함수(updater function)를 사용합니다.
function handleClick() {
// c는 최신 state 값을 받음
setCount(c => c + 1); // 0 → 1
setCount(c => c + 1); // 1 → 2
setCount(c => c + 1); // 2 → 3
// 결과: count = 3
}
state는 컴포넌트 인스턴스에 속한다
같은 컴포넌트를 여러 번 렌더링하면 각각 독립적인 state를 가집니다.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
function App() {
return (
<>
<Counter /> {/* 이 Counter의 count */}
<Counter /> {/* 저 Counter의 count — 완전히 독립적 */}
</>
);
}
두 Counter의 버튼을 각각 클릭해도 서로 영향을 주지 않습니다.
state 업데이트는 배치(Batch) 처리된다
React 18부터는 이벤트 핸들러 안의 모든 setState 호출이 일괄 처리(batching)됩니다. 여러 setState를 호출해도 리렌더링은 한 번만 발생합니다.
function handleClick() {
setFirstName('Alice'); // 리렌더 예약
setLastName('Smith'); // 리렌더 예약
setAge(30); // 리렌더 예약
// 세 setState가 하나의 리렌더로 합쳐짐 → 성능 최적화
}
다음 글에서는 useState Hook을 직접 사용하는 구체적인 방법을 살펴봅니다.
지난 글: 이벤트 처리
다음 글: useState로 상태 관리하기
읽어주셔서 감사합니다. 😊