리액트 Context API 전역 상태 관리: 드릴링 끝내는 실전 패턴

리액트 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 클릭 시 라이트/다크 배경·텍스트가 전역으로 바뀌는 모습.

리액트 Context API 예제 실행 화면

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 순서로 개선.

참고 링크

함께 보면 좋은 게시글

위로 스크롤