Context API
4. Context API
React의 Context API는 컴포넌트 트리를 통해 데이터를 props drilling 없이 전달할 수 있게 해주는 기능입니다. 전역 상태 관리나 테마, 사용자 정보 등을 여러 컴포넌트에서 공유할 때 유용합니다.
Props Drilling 문제
Props Drilling이란?
// Props를 여러 레벨에 걸쳐 전달해야 하는 문제
function App() {
const user = { name: '김개발', role: 'admin' };
return <Header user={user} />;
}
function Header({ user }) {
return (
<div>
<Navigation user={user} />
</div>
);
}
function Navigation({ user }) {
return (
<nav>
<UserMenu user={user} />
</nav>
);
}
function UserMenu({ user }) {
return (
<div>
<p>안녕하세요, {user.name}님</p>
<p>권한: {user.role}</p>
</div>
);
}
위의 예제에서 user 데이터는 App → Header → Navigation → UserMenu로 3단계를 거쳐 전달됩니다. Header와 Navigation은 실제로 user 데이터를 사용하지 않지만, 단순히 전달만 하고 있습니다.
Context API 기본 사용법
Context 생성하고 사용하기
import { createContext, useContext } from 'react';
// 1. Context 생성
const UserContext = createContext();
// 2. Provider로 데이터 제공
function App() {
const user = { name: '김개발', role: 'admin' };
return (
<UserContext.Provider value={user}>
<Header />
</UserContext.Provider>
);
}
function Header() {
return (
<div>
<h1>헤더</h1>
<Navigation />
</div>
);
}
function Navigation() {
return (
<nav>
<p>네비게이션</p>
<UserMenu />
</nav>
);
}
// 3. useContext로 데이터 사용
function UserMenu() {
const user = useContext(UserContext);
return (
<div>
<p>안녕하세요, {user.name}님</p>
<p>권한: {user.role}</p>
</div>
);
}
기본값 설정하기
import { createContext, useContext } from 'react';
// Context 생성 시 기본값 설정
const UserContext = createContext({
name: '게스트',
role: 'user'
});
// Provider 없이도 기본값 사용 가능
function GuestComponent() {
const user = useContext(UserContext);
return (
<div>
<p>사용자: {user.name}</p>
<p>권한: {user.role}</p>
</div>
);
}
테마 Context 예제
다크모드/라이트모드 구현
import { createContext, useContext, useState } from 'react';
// 테마 Context 생성
const ThemeContext = createContext();
// 테마 Provider 컴포넌트
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// 테마 사용을 위한 커스텀 Hook
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme은 ThemeProvider 내부에서 사용해야 합니다');
}
return context;
}
// 헤더 컴포넌트
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header>
<h1>My App</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? '다크 모드' : '라이트 모드'}
</button>
</header>
);
}
// 메인 컨텐츠
function MainContent() {
const { theme } = useTheme();
return (
<main>
<h2>메인 컨텐츠</h2>
<p>현재 테마: {theme}</p>
<Card />
</main>
);
}
// 카드 컴포넌트
function Card() {
const { theme } = useTheme();
return (
<div>
<h3>카드 제목</h3>
<p>카드 내용입니다. 테마에 따라 색상이 변경됩니다.</p>
</div>
);
}
// 앱 컴포넌트
function App() {
return (
<ThemeProvider>
<div>
<Header />
<MainContent />
</div>
</ThemeProvider>
);
}
여러 Context 사용하기
사용자 정보와 설정을 분리
import { createContext, useContext, useState } from 'react';
// 사용자 Context
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState({
name: '김개발',
email: 'kim@example.com',
isLoggedIn: true
});
const login = (name, email) => {
setUser({ name, email, isLoggedIn: true });
};
const logout = () => {
setUser({ name: '', email: '', isLoggedIn: false });
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// 설정 Context
const SettingsContext = createContext();
function SettingsProvider({ children }) {
const [settings, setSettings] = useState({
language: 'ko',
notifications: true,
autoSave: true
});
const updateSetting = (key, value) => {
setSettings(prev => ({
...prev,
[key]: value
}));
};
return (
<SettingsContext.Provider value={{ settings, updateSetting }}>
{children}
</SettingsContext.Provider>
);
}
// 사용자 정보 표시 컴포넌트
function UserProfile() {
const { user, logout } = useContext(UserContext);
if (!user.isLoggedIn) {
return <div>로그인이 필요합니다.</div>;
}
return (
<div>
<h3>사용자 프로필</h3>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<button onClick={logout}>로그아웃</button>
</div>
);
}
// 설정 패널
function SettingsPanel() {
const { settings, updateSetting } = useContext(SettingsContext);
return (
<div>
<h3>설정</h3>
<label>
언어:
<select
value={settings.language}
onChange={(e) => updateSetting('language', e.target.value)}
>
<option value="ko">한국어</option>
<option value="en">English</option>
</select>
</label>
<br />
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => updateSetting('notifications', e.target.checked)}
/>
알림 받기
</label>
<br />
<label>
<input
type="checkbox"
checked={settings.autoSave}
onChange={(e) => updateSetting('autoSave', e.target.checked)}
/>
자동 저장
</label>
</div>
);
}
// 앱 컴포넌트
function MultiContextApp() {
return (
<UserProvider>
<SettingsProvider>
<div>
<UserProfile />
<SettingsPanel />
</div>
</SettingsProvider>
</UserProvider>
);
}
Context 최적화
Context 분리로 불필요한 리렌더링 방지
import { createContext, useContext, useState, memo } from 'react';
// 값과 액션을 분리한 Context
const CountValueContext = createContext();
const CountActionsContext = createContext();
function CountProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(0);
return (
<CountValueContext.Provider value={count}>
<CountActionsContext.Provider value={{ increment, decrement, reset }}>
{children}
</CountActionsContext.Provider>
</CountValueContext.Provider>
);
}
// 카운트 값만 사용하는 컴포넌트
const CountDisplay = memo(function CountDisplay() {
const count = useContext(CountValueContext);
console.log('CountDisplay 렌더링');
return <h2>카운트: {count}</h2>;
});
// 액션만 사용하는 컴포넌트 (카운트 변경 시 리렌더링되지 않음)
const CountControls = memo(function CountControls() {
const { increment, decrement, reset } = useContext(CountActionsContext);
console.log('CountControls 렌더링');
return (
<div>
<button onClick={increment}>증가</button>
<button onClick={decrement}>감소</button>
<button onClick={reset}>리셋</button>
</div>
);
});
function OptimizedApp() {
return (
<CountProvider>
<div>
<CountDisplay />
<CountControls />
</div>
</CountProvider>
);
}
실습: 장바구니 애플리케이션
Context를 활용한 전역 상태 관리
import { createContext, useContext, useReducer } from 'react';
// 장바구니 상태 관리를 위한 reducer
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
} else {
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
}
// 장바구니 Context
const CartContext = createContext();
// 장바구니 Provider
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
if (quantity <= 0) {
removeItem(id);
} else {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
}
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotalPrice = () => {
return state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getTotalItems = () => {
return state.items.reduce((total, item) => total + item.quantity, 0);
};
return (
<CartContext.Provider value={{
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
totalPrice: getTotalPrice(),
totalItems: getTotalItems()
}}>
{children}
</CartContext.Provider>
);
}
// 장바구니 Hook
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart는 CartProvider 내부에서 사용해야 합니다');
}
return context;
}
// 제품 목록 컴포넌트
function ProductList() {
const { addItem } = useCart();
const products = [
{ id: 1, name: '노트북', price: 1500000 },
{ id: 2, name: '마우스', price: 50000 },
{ id: 3, name: '키보드', price: 100000 },
{ id: 4, name: '모니터', price: 300000 }
];
return (
<div>
<h2>제품 목록</h2>
{products.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>가격: {product.price.toLocaleString()}원</p>
<button onClick={() => addItem(product)}>
장바구니 추가
</button>
</div>
))}
</div>
);
}
// 장바구니 컴포넌트
function Cart() {
const { items, removeItem, updateQuantity, clearCart, totalPrice } = useCart();
if (items.length === 0) {
return (
<div>
<h2>장바구니</h2>
<p>장바구니가 비어있습니다.</p>
</div>
);
}
return (
<div>
<h2>장바구니</h2>
{items.map(item => (
<div key={item.id}>
<h4>{item.name}</h4>
<p>가격: {item.price.toLocaleString()}원</p>
<p>
수량:
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
{item.quantity}
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
</p>
<button onClick={() => removeItem(item.id)}>삭제</button>
</div>
))}
<div>
<h3>총 금액: {totalPrice.toLocaleString()}원</h3>
<button onClick={clearCart}>장바구니 비우기</button>
</div>
</div>
);
}
// 헤더 컴포넌트 (장바구니 아이템 수 표시)
function Header() {
const { totalItems } = useCart();
return (
<header>
<h1>쇼핑몰</h1>
<p>장바구니 아이템: {totalItems}개</p>
</header>
);
}
// 메인 앱
function ShoppingApp() {
return (
<CartProvider>
<div>
<Header />
<ProductList />
<Cart />
</div>
</CartProvider>
);
}
Context API 사용 시 주의사항
성능 고려사항
// ❌ 피해야 할 패턴 - 객체를 직접 전달하면 매번 새 객체 생성
function BadProvider({ children }) {
const [user, setUser] = useState({ name: '김개발' });
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// ✅ 좋은 패턴 - 값을 메모이제이션
function GoodProvider({ children }) {
const [user, setUser] = useState({ name: '김개발' });
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// 또는 useState에서 값과 setter를 분리
function BetterProvider({ children }) {
const [user, setUser] = useState({ name: '김개발' });
return (
<UserContext.Provider value={user}>
<UserActionsContext.Provider value={setUser}>
{children}
</UserActionsContext.Provider>
</UserContext.Provider>
);
}
정리
Context API의 핵심 개념을 학습했습니다:
- Props Drilling 해결: 중간 컴포넌트를 거치지 않고 데이터 전달
- createContext와 Provider: Context 생성과 데이터 제공
- useContext: Context 데이터 사용
- 커스텀 Hook: Context 사용을 위한 편리한 Hook 생성
- 여러 Context: 관련된 데이터별로 Context 분리
- 성능 최적화: Context 분리와 메모이제이션으로 리렌더링 최소화
- 실전 활용: 장바구니 같은 전역 상태 관리
Context API는 전역 상태가 필요한 경우에 유용하지만, 남용하면 컴포넌트 간의 결합도가 높아질 수 있습니다. 적절한 수준에서 사용하고, 복잡한 상태 관리가 필요한 경우에는 Redux나 Zustand 같은 전용 상태 관리 라이브러리를 고려해보세요.
다음 장에서는 에러 바운더리에 대해 알아보겠습니다.