리프팅 스테이트 업(Lifting State Up) 정리
리프팅 스테이트 업은 React에서 사용되는 중요한 디자인 패턴으로, 하위 컴포넌트들 간에 데이터를 공유하기 위해 상태(state)를 상위 컴포넌트로 끌어올리는 기법이다.
기본 개념
- 정의: 여러 자식 컴포넌트에서 동일한 데이터를 사용해야 할 때, 그 데이터를 관리하는 상태를 가장 가까운 공통 부모 컴포넌트로 이동시키는 것
- 목적:
- 여러 컴포넌트 간에 일관된 상태 유지
- 단방향 데이터 흐름 유지
- 상태 관리 로직 집중화
실제 예시: 온도 변환기
아래는 섭씨와 화씨 온도를 상호 변환하는 컴포넌트에서 리프팅 스테이트 업을 적용한 핵심 코드다.
1. 부모 컴포넌트에서 상태 관리
function Calculator() {
// 상태를 부모 컴포넌트에서 관리
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
const handleCelsiusChange = (temperature) => {
setScale('c');
setTemperature(temperature);
};
const handleFahrenheitChange = (temperature) => {
setScale('f');
setTemperature(temperature);
};
// 나머지 코드...
}
부모 컴포넌트에서 temperature와 scale 상태를 관리한다. 두 입력 필드가 공유해야 하는 데이터이기 때문에 상태를 끌어올린 것이다.
2. 자식 컴포넌트에 props로 전달
function Calculator() {
// 앞의 상태 관리 코드...
// 화면에 표시할 온도값 계산
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
</div>
);
}
부모 컴포넌트는 상태와 이벤트 핸들러를 자식 컴포넌트에 props로 전달한다. 각 입력 필드는 독립적으로 보이지만 실제로는 부모를 통해 서로 동기화된다.
3. 자식 컴포넌트에서 이벤트 처리
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
const handleChange = (e) => {
onTemperatureChange(e.target.value);
};
return (
<fieldset>
<legend>{scale === 'c' ? '섭씨' : '화씨'} 온도 입력:</legend>
<input
value={temperature}
onChange={handleChange}
/>
</fieldset>
);
}
자식 컴포넌트는 자체 상태를 갖지 않고, 부모로부터 받은 props를 사용한다. 변경 사항이 발생하면 부모로부터 받은 콜백 함수(onTemperatureChange)를 호출하여 부모 컴포넌트의 상태를 업데이트한다.
장바구니 예시
장바구니 시스템에서도 같은 패턴을 적용할 수 있다.
1. 부모 컴포넌트에서 상태 선언
function ShopApp() {
// 장바구니 상태를 부모 컴포넌트에서 관리
const [cartItems, setCartItems] = useState([]);
// 나머지 코드...
}
장바구니 상태는 여러 컴포넌트(상품 목록, 장바구니, 주문 요약)에서 필요하므로 공통 부모 컴포넌트에서 관리한다.
2. 상태 변경 함수 정의
// 장바구니에 상품 추가 함수
const handleAddToCart = (product) => {
const existingItem = cartItems.find(item => item.id === product.id);
if (existingItem) {
// 이미 있다면 수량만 증가
setCartItems(cartItems.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
));
} else {
// 없다면 새로 추가
setCartItems([...cartItems, { ...product, quantity: 1 }]);
}
};
// 장바구니에서 상품 제거 함수
const handleRemoveFromCart = (productId) => {
// 제거 로직...
};
상태를 변경하는 함수도 상태와 함께 부모 컴포넌트에 정의된다. 이 함수들은 자식 컴포넌트에 props로 전달되어 호출된다.
3. 자식 컴포넌트에 상태와 함수 전달
return (
<div className="shop-app">
<h1>리액트 쇼핑몰</h1>
<div className="app-container">
{/* 상품 목록 컴포넌트 */}
<ProductList
products={products}
onAddToCart={handleAddToCart}
/>
{/* 장바구니 컴포넌트 */}
<Cart
items={cartItems}
onRemoveFromCart={handleRemoveFromCart}
/>
{/* 결제 요약 컴포넌트 */}
<OrderSummary items={cartItems} />
</div>
</div>
);
각 자식 컴포넌트는 필요한 상태와 상태 변경 함수를 props로 받는다.
4. 자식 컴포넌트에서 props 사용 예
// 상품 목록 컴포넌트
function ProductList({ products, onAddToCart }) {
return (
<div className="product-list">
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - {product.price}원
<button onClick={() => onAddToCart(product)}>
장바구니에 추가
</button>
</li>
))}
</ul>
</div>
);
}
// 장바구니 총액 계산 예
function Cart({ items, onRemoveFromCart }) {
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
// 나머지 렌더링 로직...
}
ProductList 컴포넌트는 상품을 장바구니에 추가하기 위해 부모로부터 받은 onAddToCart 함수를 사용한다. Cart와 OrderSummary 컴포넌트는 장바구니 상태에 접근하여 필요한 계산을 수행한다.
리프팅 스테이트 업의 실전 팁
- 상태 위치 결정하기:
- 해당 상태를 사용하는 모든 컴포넌트의 가장 가까운 공통 조상을 찾아라
- 적절한 공통 조상이 없다면 상태만을 위한 새 컴포넌트를 만들어서 계층 구조 상위에 추가할 수도 있다
- props 전달 최소화:
- props drilling이 심해지면 Context API나 상태 관리 라이브러리 사용 고려
- 컴포넌트 재사용성:
- 상태 관리 로직과 UI 렌더링 로직을 분리하면 재사용성이 향상된다
- 프레젠테이션 컴포넌트와 컨테이너 컴포넌트 패턴 활용
- 성능 최적화:
- React.memo나 useMemo, useCallback 훅을 사용해 불필요한 리렌더링 방지
- React.memo나 useMemo, useCallback 훅을 사용해 불필요한 리렌더링 방지
결론
리프팅 스테이트 업은 React의 기본적인 상태 관리 패턴으로, 여러 컴포넌트 간에 데이터를 동기화할 때 매우 유용하다. 단순한 애플리케이션에서는 이 패턴만으로도 효과적인 상태 관리가 가능하지만, 규모가 커지면 Context API나 Redux 같은 추가적인 도구를 고려해야 한다.