리액트 Context API 전역 상태 관리: 드릴링 끝내는 실전 패턴
리액트 Context API는 트리 어디서든 값을 꺼내 쓰게 해주는 전역 전달 메커니즘입니다. createContext → Provider → useContext 흐름만 알면, 테마·언어·인증 같은 공통 값을 props 드릴링 없이 공유할 수 있습니다. 이 글에서는 최소 예제와 테마 토글 데모를 통해 기본 사용법과 실무 팁(리렌더 최소화, 안전한 커스텀 훅)을 간결하게 정리합니다.
왜 Context인가? (props 드릴링의 끝)
// 드릴링 예: 상위 → 중간 → 하위로 같은 props를 계속 전달
<Layout theme={theme}>
<Header theme={theme} />
<Main theme={theme} />
</Layout>Context를 쓰면 중간 단계가 props를 받을 필요가 없습니다.
// 전역 공급자 1번만 감싸고, 하위에선 useContext로 직접 접근
<ThemeProvider>
<Layout>
<Header />
<Main />
</Layout>
</ThemeProvider>1) 최소 예제: createContext / Provider / useContext
파일 (예: src/context/theme.js)
import React, { createContext, useContext, useState, useMemo } from "react";
const ThemeContext = createContext(null);
// 디버깅에 도움
ThemeContext.displayName = "ThemeContext";
// 안전한 접근용 커스텀 훅
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx; // { theme, setTheme, toggle }
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
// value는 메모이제이션하여 불필요 리렌더 최소화
const value = useMemo(() => ({ theme, setTheme, toggle }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}사용 (예: src/components/Header.jsx)
import React, { createContext, useContext, useState, useMemo } from "react";
const ThemeContext = createContext(null);
// 디버깅에 도움
ThemeContext.displayName = "ThemeContext";
// 안전한 접근용 커스텀 훅
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx; // { theme, setTheme, toggle }
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
// value는 메모이제이션하여 불필요 리렌더 최소화
const value = useMemo(() => ({ theme, setTheme, toggle }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}예제 (예: src/examples/ContextThemeDemo.jsx)
import React from "react";
import { ThemeProvider } from "../context/theme";
import Header from "../components/Header";
export default function ContextThemeDemo() {
return (
<ThemeProvider>
<div>
<Header />
<main style={{ padding: 16 }}>
<p>아래 버튼으로 라이트/다크 테마를 전역으로 토글해 보세요.</p>
</main>
</div>
</ThemeProvider>
);
}▼ 실행 화면: Toggle 클릭 시 라이트/다크 배경·텍스트가 전역으로 바뀌는 모습.

2) 성능 최적화: value 메모이제이션·Context 분리·Selector 아이디어
- value는 안정 참조:
<Provider value={{...}}>에 새 객체를 매 렌더 넣지 말고,useMemo로 감싸거나dispatch처럼 안정 참조만 전달. - Context 분리: 자주 바뀌는 값(입력값, 스크롤 위치)과 거의 고정인 값(설정, 권한)을 서로 다른 Provider로 분리해 리렌더 범위를 축소.
- Selector 패턴(고급): Context 전체 대신 필요한 조각만 구독하도록 선택자 훅을 두면 리렌더를 더 줄일 수 있음(서드파티 라이브러리에서 흔히 제공).
3) 적합한 사용처 & 다른 라이브러리와의 경계
- Context 적합: 테마/언어/인증/환경설정처럼 “여러 곳에서 읽되 변경 빈도가 낮은 값”.
- 별도 라이브러리 고려: 대규모 교차 모듈 상태, 고빈도 업데이트, 개발자 도구/비동기 캐시/미들웨어가 필요하면 Redux Toolkit, Zustand, Recoil 등을 평가.
체크리스트
- Provider는 앱 루트에 한 번만 두는 것부터 시작하고, 필요한 경우 영역별로 세분화.
useXxx()커스텀 훅으로 Provider 밖 사용 시 에러를 조기 감지.- Reducer 패턴을 쓰면 액션/로직이 분리되어 유지보수가 쉬움.
- 리렌더 이슈가 보이면: value 메모이제이션 → Context 분리 → Selector 순서로 개선.
참고 링크
함께 보면 좋은 게시글
- 리액트 props 사용법: 부모에서 자식으로 데이터 전달하기
- 리액트 state 관리: useState 기본 사용법과 동작 원리
- 리액트 useReducer vs useState: 언제 쓰고 어떻게 마이그레이션할까
- 리액트 useMemo 제대로 쓰기: 언제 이득이고 언제 오버엔지니어링일까
- 리액트 useCallback 제대로 쓰기: 참조 안정화로 자식 리렌더 줄이기
이 글이 도움이 되셨다면 공유 부탁 드립니다.



