Context API의 매력과 숨겨진 비용
React에서 Props Drilling을 해결하기 위해 Context API는 매우 직관적인 해결책으로 보입니다. 복잡한 외부 라이브러리 없이 전역 상태를 관리할 수 있기 때문입니다.
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
return (
<AuthContext.Provider value={{ currentUser, setCurrentUser }}>
{children}
</AuthContext.Provider>
);
};
export const UserBadge = () => {
const { currentUser } = useContext(AuthContext);
if (!currentUser) return null;
return <span>{currentUser.nickname}</span>;
};
Context API를 주의해야 하는 이유
1. 불필요한 리렌더링으로 인한 성능 저하
Context Provider의 value가 변경되면, 해당 Context를 구독하는 모든 컴포넌트는 실제로 사용하는 데이터가 변경되지 않았더라도 강제로 리렌더링됩니다.
const SettingsContext = createContext(null);
export const SettingsProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('ko');
// theme만 변경되어도 language를 사용하는 컴포넌트까지 리렌더링됨
return (
<SettingsContext.Provider value={{ theme, language, setTheme, setLanguage }}>
{children}
</SettingsContext.Provider>
);
};
2. 컴포넌트 결합도 증가와 디버깅의 어려움
특정 Context에 강하게 의존하는 컴포넌트는 다른 프로젝트나 위치로 이동시키기 어렵습니다. 또한, 컴포넌트 트리가 깊어질수록 상태가 어디서 변이되었는지 추적하는 것이 매우 까다로워집니다.
현대적인 상태 관리 및 통신 대안
1. 컴포넌트 합성 (Component Composition)
상태를 전역으로 끌어올리기 전에, 컴포넌트 구조를 재설계하여 Props Drilling 자체를 피할 수 있는지 검토해야 합니다.
const DashboardLayout = ({ header, sidebar, mainContent }) => {
return (
<div className="dashboard">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{mainContent}</main>
</div>
);
};
// 사용 예시: 상태를 최상위에서 관리하며 자식에게 직접 전달
const App = () => {
const [metrics, setMetrics] = useState({});
return (
<DashboardLayout
header={<Header title="Dashboard" />}
sidebar={<Sidebar metrics={metrics} />}
mainContent={<Chart metrics={metrics} onUpdate={setMetrics} />}
/>
);
};
2. Zustand: 최소한의 보일러플레이트
Zustand는 Selector 패턴을 통해 구독한 상태가 변경될 때만 컴포넌트를 리렌더링하므로 Context의 성능 문제를 완벽히 해결합니다.
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
totalPrice: 0,
addItem: (product) => set((state) => ({
items: [...state.items, product],
totalPrice: state.totalPrice + product.price
})),
clearCart: () => set({ items: [], totalPrice: 0 })
}));
// CartIcon은 items의 길이만 구독하므로, totalPrice 변경 시 리렌더링되지 않음
const CartIcon = () => {
const itemCount = useCartStore((state) => state.items.length);
return <button>Cart ({itemCount})</button>;
};
3. Jotai: 상향식(Bottom-up) 원자적 상태 관리
Jotai는 상태를 독립적인 'Atom' 단위로 쪼개어 관리하며, React의 기본 상태 관리 방식과 매우 유사한 개발자 경험을 제공합니다.
import { atom, useAtom } from 'jotai';
const searchQueryAtom = atom('');
const filterOptionsAtom = atom({ category: 'all', inStock: true });
const SearchBar = () => {
const [query, setQuery] = useAtom(searchQueryAtom);
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
);
};
4. 이벤트 기반 통신 (Event-Driven Architecture)
마이크로 프론트엔드나 완전히 분리된 모듈 간에는 상태 공유 대신 이벤트 발행/구독 패턴이 더 적합할 수 있습니다.
class EventEmitter {
constructor() {
this.listeners = new Map();
}
subscribe(eventName, callback) {
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, []);
}
this.listeners.get(eventName).push(callback);
// 구독 해제 함수 반환
return () => {
const callbacks = this.listeners.get(eventName);
this.listeners.set(eventName, callbacks.filter(cb => cb !== callback));
};
}
publish(eventName, payload) {
const callbacks = this.listeners.get(eventName) || [];
callbacks.forEach(cb => cb(payload));
}
}
export const globalEventBus = new EventEmitter();
// 컴포넌트 A에서 이벤트 발생
globalEventBus.publish('analytics:track', { action: 'click', target: 'buy_button' });
// 컴포넌트 B에서 이벤트 수신
useEffect(() => {
const unsubscribe = globalEventBus.subscribe('analytics:track', (data) => {
sendToServer(data);
});
return unsubscribe;
}, []);
프로젝트 규모에 따른 아키텍처 선택 가이드
- 소규모 및 단순한 UI: 컴포넌트 합성(Component Composition)과 지역 상태(Lifting State Up) 활용.
- 중규모 및 인터랙티브한 앱: Zustand 또는 Jotai를 도입하여 성능과 개발 생산성 동시 확보.
- 대규모 엔터프라이즈 앱: Redux Toolkit 또는 RTK Query를 사용하여 예측 가능한 상태 흐름과 미들웨어 생태계 활용.
- 마이크로 프론트엔드 및 모듈 분리: Custom Events 또는 Event Bus를 통한 느슨한 결합(Loose Coupling) 유지.
상태 관리 설계 원칙
상태 관리 도구를 도입할 때는 항상 '상태의 수명과 범위'를 먼저 고려해야 합니다. 전역 상태는 최소화하고, 상태는 가능한 한 그것을 사용하는 컴포넌트와 물리적으로 가깝게 배치하는 것이 유지보수성을 높이는 핵심입니다. 또한, UI 상태와 서버 상태(React Query, SWR 등)를 명확히 분리하면 불필요한 전역 상태 관리 코드를 획기적으로 줄일 수 있습니다.