React 교재 · 중급 13편

React Context API — 전역 상태

같은 값을 props 로 5단계 내려보내는 지옥 끝내기. Context + useContext 한 쌍.

중앙 데이터 구슬에서 깊이 중첩된 UI 카드들에 값이 흘러내리는 일러스트 — Context 공유 상태 컨셉

App 컴포넌트가 사용자 정보(user)를 가지고 있고, 화면 깊숙한 UserMenu 컴포넌트가 그걸 필요로 한다. 사이에 Layout·Header·Nav 가 있는데, 셋 다 user 를 안 쓰지만 단지 통과시키기 위해 prop 으로 받아 다시 내려보낸다. 이게 prop drilling — 그리고 React Context 가 푸는 문제.

이번 13편은 Context 의 3 부품(createContext·Provider·useContext) + 언제 써야 하고 언제 쓰면 안 되는지 + 성능 함정 + 회사 stack 에 두기 좋은 대안 (Zustand) 까지.

1. 3 부품 — createContext · Provider · useContext

// 1. Context 생성 import { createContext, useContext } from 'react'; const ThemeContext = createContext<'light' | 'dark'>('light'); // 2. Provider — 값 공급 function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={theme}> <Layout /> </ThemeContext.Provider> ); } // 3. useContext — 어느 깊이에서든 읽기 function DeepButton() { const theme = useContext(ThemeContext); return <button className={theme}>...</button>; }

Layout·Header·중간 컴포넌트들이 theme 을 prop 으로 받지 않는다. DeepButtonuseContext 한 줄로 직접 접근. prop drilling 5단계가 0단계.

핵심 이해 — Context 가 마법은 아니다. React 가 컴포넌트 트리를 올라가며 가장 가까운 Provider 를 찾아 그 value 를 반환할 뿐. Provider 가 없으면 createContext 의 기본값 사용. Provider 가 여러 개면 가장 가까운 것 우선.

2. 진짜 패턴 — value + updater 같이 공급

위 예제는 theme 만 공급. 실전에선 값 + 변경 함수를 객체로 묶어 공급:

type ThemeContextType = { theme: 'light' | 'dark'; toggle: () => void; }; const ThemeContext = createContext<ThemeContextType | null>(null); export function ThemeProvider({ children }) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light'); return ( <ThemeContext.Provider value={{ theme, toggle }}> {children} </ThemeContext.Provider> ); } // 편의 Hook export function useTheme() { const ctx = useContext(ThemeContext); if (!ctx) throw new Error('useTheme must be inside ThemeProvider'); return ctx; } // 사용 function ThemeToggle() { const { theme, toggle } = useTheme(); return <button onClick={toggle}>{theme}</button>; }

이 패턴 3가지 장점 — ① 같은 컨텍스트에서 값·변경 함수를 한 번에. ② 커스텀 Hook (useTheme) 이 Provider 없이 사용하는 실수를 컴파일/런타임 둘 다에서 잡음. ③ 12편 커스텀 Hook 와 자연스러운 결합.

3. 성능 함정 — Provider value 가 매 렌더마다 새 객체

위 코드의 value={{ theme, toggle }} 가 사고 지점. 부모 컴포넌트가 다른 이유로 재렌더되면 매번 새 객체 가 만들어진다 → Context 구독자 전부 재렌더. theme 이 안 바뀌어도.

해결 — useMemo 로 객체 메모이즈 (16편 자세히):

const value = useMemo(() => ({ theme, toggle }), [theme]); return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;

또는 — state 와 dispatch 를 다른 Context 두 개로 분리. 값만 필요한 컴포넌트는 state Context, 변경만 필요한 컴포넌트는 dispatch Context 구독. dispatch 는 안 바뀌니 재렌더 0. Redux 가 옛날부터 쓰던 패턴.

4. Context 의 한계 — Zustand 가 답일 때

Context 가 적합한 경우 — 잘 안 바뀌는 전역 값 (theme·locale·로그인 사용자·인증 토큰). 자주 바뀌는 state (장바구니 항목 수·실시간 알림 카운트) 에 Context 를 쓰면 위 성능 함정이 일상.

그런 경우 — Zustand · Jotai · TanStack Store 같은 외부 상태 관리. 회사 stack 에 Zustand 가 자주 들어가는 이유다.

// Zustand 예시 — Context 보다 간단 import { create } from 'zustand'; const useCartStore = create((set) => ({ items: [], add: (item) => set((s) => ({ items: [...s.items, item] })), })); function CartCount() { const count = useCartStore((s) => s.items.length); // ← selector return <span>{count}</span>; }

핵심 — Zustand 는 selector 로 "이 컴포넌트는 items.length 만 본다" 명시. items 배열 자체가 바뀌어도 length 가 같으면 재렌더 0. Context 가 못 하는 일.

실전 가이드 — 작은 앱 + 잘 안 바뀌는 값 → Context. 큰 앱 + 자주 바뀌는 값 → Zustand (또는 Redux Toolkit). 둘 다 쓰는 게 정상 — 정답은 둘 중 하나가 아님.

13편으로 컴포넌트 간 데이터 공유 패턴이 완성. 14편 useReducer 는 그 안에서 다루는 state 가 복잡해질 때 useState 의 진화형. Context + useReducer 조합이 Redux 직전까지 커버한다.

다음 글

React 교재 14편 — useReducer. 복잡한 state + action + reducer 패턴, useState 와 언제 갈아탈지 기준.

© 2026 주나이테크(주) @JUNAITECH