React에서 부모 컴포넌트가 자식 컴포넌트로 데이터를 전달할 때 props를 사용합니다.
하지만 중간에 여러 컴포넌트를 거치는 Prop Drilling 상황이 발생하면 관리가 어렵고 불편합니다.
이를 해결하기 위해 React Context API를 활용하여 데이터를 트리 전체에 손쉽게 전달하는 방법을 살펴봅니다.
Prop Drilling은 데이터가 필요한 컴포넌트에 도달하기 위해, 불필요하게 중간 컴포넌트들을 거쳐야 하는 상황을 말합니다.
Header
, Main
, Footer
컴포넌트 모두 부모 컴포넌트로부터 동일한 데이터를 받아야 합니다.darkMode
)입니다.import "./AppTheme.css";
import HeaderTheme from "./components/theme/Header.jsx";
import MainTheme from "./components/theme/Main.jsx";
import FooterTheme from "./components/theme/Footer.jsx";
import { useState } from "react";
function AppTheme() {
const [darkMode, setDarkMode] = useState(false);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<>
<HeaderTheme darkMode={darkMode} toggleDarkMode={toggleDarkMode} />
<MainTheme darkMode={darkMode} />
<FooterTheme darkMode={darkMode} />
</>
);
}
export default AppTheme;
각 컴포넌트에 darkMode
와 toggleDarkMode
를 전달합니다.
HeaderTheme.jsx
export default function Header({ darkMode, toggleDarkMode }) {
return (
<header className={`header ${darkMode ? "header--dark" : "header--light"}`}>
<h1 className="header__title">헤더 컴포넌트</h1>
<button onClick={toggleDarkMode} className="header__button">
{darkMode ? "라이트 모드로 전환" : "다크 모드로 전환"}
</button>
</header>
);
}
FooterTheme.jsx
export default function Footer({ darkMode }) {
return (
<footer className={`footer ${darkMode ? "footer--dark" : "footer--light"}`}>
<p className="footer__text">푸터 컴포넌트</p>
</footer>
);
}
Context는 부모 컴포넌트가 트리 전체에 데이터를 전달할 수 있도록 하는 React의 내장 기능입니다.
이를 사용하면 데이터가 필요한 컴포넌트에 "순간 이동"하듯 값을 전달할 수 있습니다.
src/context/DarkModeContext.jsx
import { createContext } from "react";
export const DarkModeContext = createContext(false);
DarkModeContext
를 생성합니다. 이를 통해 데이터를 공유할 수 있습니다.
AppTheme.jsx
import { useState } from "react";
import { DarkModeContext } from "./context/DarkModeContext";
import HeaderTheme from "./components/theme/Header";
import MainTheme from "./components/theme/Main";
import FooterTheme from "./components/theme/Footer";
function AppTheme() {
const [darkMode, setDarkMode] = useState(false);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
<HeaderTheme />
<MainTheme />
<FooterTheme />
</DarkModeContext.Provider>
);
}
export default AppTheme;
DarkModeContext.Provider
를 사용해 darkMode
와 toggleDarkMode
를 하위 컴포넌트로 전달합니다.useContext
로 데이터 사용하기Header.jsx
import { useContext } from "react";
import { DarkModeContext } from "../../context/DarkModeContext";
export default function Header() {
const { darkMode, toggleDarkMode } = useContext(DarkModeContext);
return (
<header className={`header ${darkMode ? "header--dark" : "header--light"}`}>
<h1 className="header__title">헤더 컴포넌트</h1>
<button onClick={toggleDarkMode} className="header__button">
{darkMode ? "라이트 모드로 전환" : "다크 모드로 전환"}
</button>
</header>
);
}
Footer.jsx
import { useContext } from "react";
import { DarkModeContext } from "../../context/DarkModeContext";
export default function Footer() {
const { darkMode } = useContext(DarkModeContext);
return (
<footer className={`footer ${darkMode ? "footer--dark" : "footer--light"}`}>
<p className="footer__text">푸터 컴포넌트</p>
</footer>
);
}
Card.jsx
import { useContext } from "react";
import { DarkModeContext } from "../context/DarkModeContext";
export default function Card({ title, children }) {
const { darkMode } = useContext(DarkModeContext);
return (
<div className={`card ${darkMode ? "card--dark" : "card--light"}`}>
<div className="card__header">{title}</div>
<div className="card__body">{children}</div>
</div>
);
}
darkMode
와 toggleDarkMode
를 사용할 수 있습니다.Header
, Footer
, Card
가 필요할 때 useContext
를 통해 데이터에 접근합니다.Provider
는 데이터를 전달.useContext
는 데이터를 사용.React의 Context API는 데이터를 컴포넌트 트리 전체에 "순간 이동"시킬 수 있는 강력한 도구입니다.
특히, 여러 컴포넌트가 공통 데이터를 공유해야 할 때 매우 유용합니다.
컨텍스트를 정의하고, DarkModeProvider
를 통해 상태와 동작을 제공합니다.
import { createContext, useState } from "react";
export const DarkModeContext = createContext();
export const DarkModeProvider = ({ children, initDarkMode = true }) => {
const [darkMode, setDarkMode] = useState(initDarkMode);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<DarkModeContext.Provider value={{ darkMode, toggleDarkMode }}>
{children}
</DarkModeContext.Provider>
);
};
DarkModeProvider
를 사용해 Context를 제공하며, 초기 값을 설정합니다.
import "./AppTheme.css";
import HeaderTheme from "./components/theme/Header.jsx";
import MainTheme from "./components/theme/Main.jsx";
import FooterTheme from "./components/theme/Footer.jsx";
import { DarkModeProvider } from "./context/DarkModeContext.jsx";
function AppTheme(props) {
return (
<DarkModeProvider initDarkMode={false}>
<HeaderTheme />
<MainTheme />
<FooterTheme />
</DarkModeProvider>
);
}
export default AppTheme;
DarkModeProvider
를 중첩하여 특정 섹션에만 별도의 Dark Mode 컨텍스트를 제공합니다.
import { DarkModeProvider } from "../../context/DarkModeContext";
import Card from "../Card";
export default function Main() {
return (
<main>
<DarkModeProvider>
<Card title="제목">
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatibus iste laborum iusto ipsa cum est dolorum pariatur eaque ipsam at, sunt exercitationem dolor magnam, et esse. Porro
laborum eligendi nemo.
</Card>
</DarkModeProvider>
</main>
);
}
이 상태에서는 DarkModeProvider
를 중첩하여 다른 상태를 관리할 수도 있습니다.
1️⃣ 자주 변경되지 않는 데이터를 Context로 사용
Context 상태가 변경되면 하위 모든 컴포넌트가 리렌더링되므로, 빈번히 업데이트되는 데이터는 Context 사용에 적합하지 않습니다.
2️⃣ 필요한 범위에서만 Context 제공
꼭 필요한 섹션에서만 Provider
를 사용하는 것이 성능과 유지보수에 유리합니다.
Context는 Prop Drilling 문제를 해결하고, 여러 컴포넌트가 공통 상태를 공유할 수 있는 강력한 도구입니다.
그러나, 사용 목적과 데이터 변경 빈도를 고려하여 적절히 활용하는 것이 중요합니다!
Reducer와 Context를 사용하면 앱 상태 관리를 더 간결하고 효율적으로 할 수 있습니다.
이 문서에서는 할 일 관리 앱을 예제로, Reducer
와 Context
를 결합하여 앱을 확장하는 방법을 심층적으로 다룹니다.
Reducer
와 Context
를 활용한 앱 구조는 다음과 같습니다.
앱의 메인 컴포넌트로, TodoProvider
를 통해 전역 상태를 제공합니다.
import "./App.css";
import TodoList from "./components/todo/TodoList";
import AddTodo from "./components/todo/AddTodo";
import { TodoProvider } from "./context/TodoContext";
function AppTodo() {
return (
<TodoProvider>
<h2>할일 목록</h2>
<AddTodo />
<TodoList />
</TodoProvider>
);
}
export default AppTodo;
새로운 할 일을 추가하는 컴포넌트로, useTodos
와 useDispatch
를 사용하여 상태를 업데이트합니다.
import { useState } from "react";
import { useDispatch, useTodos } from "../../context/TodoContext";
export default function AddTodo() {
const [todoText, setTodoText] = useState("");
const todos = useTodos();
const dispatch = useDispatch();
const handleAddTodo = (text) => {
dispatch({
type: "added",
nextId: todos.length,
todoText: text,
});
setTodoText("");
};
return (
<div>
<input
type="text"
value={todoText}
onChange={(e) => setTodoText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
handleAddTodo(todoText);
}
}}
/>
<button onClick={() => handleAddTodo(todoText)}>추가</button>
</div>
);
}
할 일 목록을 렌더링하는 컴포넌트입니다.
useTodos
를 통해 상태를 가져오고, 각 항목은 TodoItem
컴포넌트로 구성됩니다.
import TodoItem from "./TodoItem";
import { useTodos } from "../../context/TodoContext";
function TodoList() {
const todos = useTodos();
return (
<ul>
{todos.map((item) => (
<li key={item.id}>
<TodoItem item={item} />
</li>
))}
</ul>
);
}
export default TodoList;
할 일 항목을 표시하며, 완료 상태 토글과 삭제 기능을 제공합니다.
import { useDispatch } from "../../context/TodoContext";
export default function TodoItem({ item }) {
const dispatch = useDispatch();
const handleToggleTodo = (id, done) => {
dispatch({ type: "done", id, done });
};
const handleDeleteTodo = (id) => {
dispatch({ type: "deleted", id });
};
return (
<label>
<input
type="checkbox"
checked={item.done}
onChange={(e) => handleToggleTodo(item.id, e.target.checked)}
/>
<span>{item.done ? <del>{item.text}</del> : item.text}</span>
<button onClick={() => handleDeleteTodo(item.id)}>X</button>
</label>
);
}
Reducer는 상태 변경 로직을 중앙에서 관리하며, 모든 상태 업데이트가 이곳에서 이루어집니다.
export default function todoReducer(draft, action) {
switch (action.type) {
case "added":
draft.push({ id: action.nextId, text: action.todoText, done: false });
break;
case "deleted":
return draft.filter((item) => item.id !== action.id);
case "done":
const target = draft.find((item) => item.id === action.id);
if (target) target.done = action.done;
break;
default:
throw new Error(`알 수 없는 액션 타입: ${action.type}`);
}
}
TodoContext
와 TodoDispatchContext
를 생성하고, 이를 통해 상태와 디스패치를 전역적으로 관리합니다.
import { createContext, useContext } from "react";
import { useImmerReducer } from "use-immer";
import todoReducer from "../reducer/todo-reducer";
export const TodoContext = createContext(null);
export const TodoDispatchContext = createContext(null);
export function TodoProvider({ children }) {
const [todos, dispatch] = useImmerReducer(todoReducer, [
{ id: 0, text: "HTML&CSS 공부하기", done: false },
{ id: 1, text: "자바스크립트 공부하기", done: false },
]);
return (
<TodoContext.Provider value={todos}>
<TodoDispatchContext.Provider value={dispatch}>
{children}
</TodoDispatchContext.Provider>
</TodoContext.Provider>
);
}
export function useTodos() {
return useContext(TodoContext);
}
export function useDispatch() {
return useContext(TodoDispatchContext);
}
useTodos
와 useDispatch
커스텀 훅 사용TodoContext
와 TodoDispatchContext
를 직접 접근하는 대신, 커스텀 훅을 통해 간결하게 관리합니다.export function useTodos() {
return useContext(TodoContext);
}
export function useDispatch() {
return useContext(TodoDispatchContext);
}
TodoProvider
로 상태와 디스패치 일괄 제공TodoProvider
를 사용해 간결하게 구성할 수 있습니다.Reducer와 Context로 상태 관리 통합
커스텀 훅으로 가독성 향상
useTodos()
와 useDispatch()
를 사용해 상태와 디스패치를 간편하게 가져옵니다. UI와 로직의 분리
AddTodo
, TodoList
, TodoItem
)는 상태 관리 로직과 분리되어 유지보수가 쉽습니다. Immer를 활용한 불변성 유지
useImmerReducer
를 사용해 상태 변경을 더 안전하고 직관적으로 관리합니다.