[React] Context API, useReducer
개요
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 |