3 분 소요

개요

Context API는 Props를 여러 단계로 내려보내지 않고 전역으로 데이터를 공유하는 방법입니다. useReducer는 복잡한 상태 로직을 정리하는 Hook입니다. 두 가지를 함께 사용하면 외부 라이브러리 없이도 전역 상태 관리를 구현할 수 있습니다.


Props Drilling 문제

App
 └── Page
      └── Section
           └── UserProfile  ← 여기서 user 데이터 필요

App에서 UserProfile까지 단계마다 user Props를 전달해야 하는 문제를 Props Drilling이라 합니다. Context API로 해결합니다.


Context API 기본

createContext, Provider, useContext

// src/contexts/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from "react";

// 1. Context 타입 정의
interface ThemeContextType {
  theme: "light" | "dark";
  toggleTheme: () => void;
}

// 2. Context 생성 (기본값 제공)
const ThemeContext = createContext<ThemeContextType | null>(null);

// 3. Provider 컴포넌트 (데이터 제공)
export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  return (
    <ThemeContext.Provider value=>
      {children}
    </ThemeContext.Provider>
  );
}

// 4. 커스텀 Hook으로 사용 편의 제공
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error("ThemeProvider 안에서 사용해야 합니다");
  return context;
}
// src/main.tsx - Provider로 감싸기
import { ThemeProvider } from "./contexts/ThemeContext";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <ThemeProvider>
    <App />
  </ThemeProvider>
);
// src/components/Header.tsx - 어디서든 사용 가능
import { useTheme } from "../contexts/ThemeContext";

function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header style=>
      <button onClick={toggleTheme}>
        {theme === "dark" ? "라이트 모드" : "다크 모드"}
      </button>
    </header>
  );
}


useReducer

useState vs useReducer

// useState: 간단한 상태
const [count, setCount] = useState(0);
setCount(count + 1);

// useReducer: 복잡한 상태 로직
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "INCREMENT" });

기본 사용법

import { useReducer } from "react";

// 1. 상태 타입 정의
interface State {
  count: number;
}

// 2. 액션 타입 정의
type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "RESET" }
  | { type: "SET"; payload: number };

// 3. Reducer 함수 (순수 함수)
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return { count: 0 };
    case "SET":
      return { count: action.payload };
    default:
      return state;
  }
}

// 4. 컴포넌트에서 사용
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
      <button onClick={() => dispatch({ type: "RESET" })}>초기화</button>
      <button onClick={() => dispatch({ type: "SET", payload: 100 })}>100 설정</button>
    </div>
  );
}


Context API + useReducer 조합

전역 상태가 복잡할 때 두 가지를 조합합니다.

// src/contexts/TodoContext.tsx
import {
  createContext, useContext, useReducer, ReactNode
} from "react";

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

interface State {
  todos: Todo[];
}

type Action =
  | { type: "ADD"; text: string }
  | { type: "TOGGLE"; id: number }
  | { type: "DELETE"; id: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "ADD":
      return {
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.text, done: false },
        ],
      };
    case "TOGGLE":
      return {
        todos: state.todos.map((t) =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
      };
    case "DELETE":
      return {
        todos: state.todos.filter((t) => t.id !== action.id),
      };
    default:
      return state;
  }
}

const TodoContext = createContext<{
  state: State;
  dispatch: React.Dispatch<Action>;
} | null>(null);

export function TodoProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(reducer, { todos: [] });
  return (
    <TodoContext.Provider value=>
      {children}
    </TodoContext.Provider>
  );
}

export function useTodo() {
  const ctx = useContext(TodoContext);
  if (!ctx) throw new Error("TodoProvider 안에서 사용해야 합니다");
  return ctx;
}
// 사용
import { useTodo } from "../contexts/TodoContext";

function TodoList() {
  const { state, dispatch } = useTodo();

  return (
    <ul>
      {state.todos.map((todo) => (
        <li key={todo.id} style=>
          {todo.text}
          <button onClick={() => dispatch({ type: "TOGGLE", id: todo.id })}>완료</button>
          <button onClick={() => dispatch({ type: "DELETE", id: todo.id })}>삭제</button>
        </li>
      ))}
    </ul>
  );
}


Context API를 사용하지 않는 경우

Context는 리렌더링을 발생시키므로, 자주 바뀌는 상태에는 적합하지 않습니다.

상황 권장 방법
로그인 사용자 정보, 테마, 언어 Context API
복잡한 폼, 여러 컴포넌트 공유 상태 Context + useReducer
자주 변경되는 서버 데이터 TanStack Query
대규모 앱 전역 상태 Zustand


관련 링크