Context API
는 컴포넌트 트리에서 데이터를 전역적으로 공유할 수 있음.props drilling
없이도 데이터를 효율적으로 전달할 수 있음!Props driling
context를 사용하면 컴포넌트를 재사용하기 어려워지므로 꼭 필요할 때만 쓸 것
을 권장한다고 함컴포넌트 합성(composition)
이 더 간단한 해결책일 수 있음4-1. 구버전 Consumer 컴포넌트를 사용했다.
import { createContext } from ‘react’;
export const themeContext = createContext(전달할 데이터의 초기값);
export default function App() {
return (
<themeContext.Provider value={전달할 데이터}>
<Theme />
</themeContext.Provider>
)
}
function Theme() {
return (
<themeContext.Consumer>
{value => <div>{value}</div>}
</themeContext.Consumer>
)
}
4-2. 요즘 방법 useContext() 사용
import { createContext, useContext } from ‘react’;
export const themeContext = createContext(전달할 데이터의 초기값);
export default function App() {
return (
<themeContext.Provider value={전달 데이터}>
<Theme />
</themeContext.Provider>
)
}
function Theme() {
const theme = useContext(themeContext);
return <div>{theme}</div> // Provider에서 value로 전달한 데이터 출력
}
value
가 변경될 때마다 그 값을 구독하고 있는 모든 하위 컴포넌트들이 리렌더링되고. 이로 인해 대규모 애플리케이션에서 성능 이슈가 발생함.React.memo
, useMemo
를 사용해 불필요한 리렌더링을 방지할 수 있긴 함React.memo
: 컴포넌트 리렌더링을 방지하기 위해 사용useMemo
: 불필요한 값 계산을 방지하기 위해 사용useCallback
: 함수를 메모이제이션Context를 분리
ex) 상태 공급 컨텍스트와 소비 컨텍스트로 분리
// 공급 컨텍스트 (상태 관리)
const CounterContext = createContext<number | null>(null);
// 소비 컨텍스트 (상태 변경 함수 관리)
const CounterActionContext = createContext<{
increase: () => void;
decrease: () => void;
reset: () => void;
} | null>(null);
useMemo와 useCallback 훅을 활용
Context를 사용하는 컴포넌트에 React.memo를 적용
//Todo.ts
import { createContext } from "react";
export const ThemeContext = createContext<{ isDarkMode: boolean } | null>(
null
);
export const ThemeActionContext = createContext<
| {
toggleDarkMode: () => void;
}
| null
>(null);
ThemeContext
: isDarkMode
값을 저장하는 상태를 위한 컨텍스트ThemeActionContext
: 상태를 변경하는 toggleDarkMode
함수가 저장되는 컨텍스트
// TodoProvider.tsx
import { useMemo, useState, ReactNode } from "react";
import { ThemeActionContext, ThemeContext } from "../Todo";
export default function TodoProvider({ children }: { children: ReactNode }) {
const [isDarkMode, setIsDarkMode] = useState(false);
const toggleDarkMode = () => {
setIsDarkMode((prev) => !prev);
};
const memo = useMemo(() => ({ toggleDarkMode }), []);
return (
<>
<ThemeActionContext.Provider value={memo}>
<ThemeContext.Provider value={{ isDarkMode }}>
{children}
</ThemeContext.Provider>
</ThemeActionContext.Provider>
</>
);
}
isDarkMode
⇒ useState
를 통해 다크 모드의 활성화 여부를 저장toggleDarkMode
⇒ isDarkMode
를 토글(반전)useMemo
⇒ toggleDarkMode
함수 객체를 메모이제이션하여 불필요한 재생성을 방지Provider
return 함// 커스텀 훅
// useTheme.tsx
import { useContext } from "react";
import { ThemeContext, ThemeActionContext } from "./Todo";
export const useTheme = () => {
const themeContext = useContext(ThemeContext);
const themeActionContext = useContext(ThemeActionContext);
if (!themeContext || !themeActionContext) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return { ...themeContext, ...themeActionContext };
};
ThemeContext
에서 isDarkMode
상태를 가져옴.ThemeActionContext
에서 toggleDarkMode
함수를 가져옴useTheme
훅은 반드시 TodoProvider
내에서 호출되어야 하며, 그렇지 않을 경우 에러를 발생시킴{ ...themeContext, ...themeActionContext }
형태로 상태와 액션을 결합하여 반환// TodoHeader.tsx
import { useTheme } from "../context/useTheme";
export default function TodoHeader() {
const { isDarkMode, toggleDarkMode } = useTheme();
return (
<header
className={`p-4 text-center ${
isDarkMode ? "bg-gray-800 text-white" : "bg-blue-500 text-black"
}`}
>
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Todo List</h1>
<button
className="bg-gray-300 p-2 rounded hover:bg-gray-400 transition-colors"
onClick={toggleDarkMode}
>
{isDarkMode ? "Light Mode" : "Dark Mode"}
</button>
</div>
</header>
);
}
//App.tsx
import { useEffect } from "react";
import Todo from "./components/Todo";
import { useTheme } from "./context/useTheme";
import "./css/index.css";
const App = () => {
const { isDarkMode, toggleDarkMode } = useTheme();
useEffect(() => {
document.body.className = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
return (
<div>
<button onClick={toggleDarkMode}>
{isDarkMode ? "Switch to Light Mode" : "Switch to Dark Mode"}
</button>
<Todo />
</div>
);
};
export default App;
//main.tsx
import ReactDOM from "react-dom/client";
import App from "./App";
import TodoProvider from "./context/provider/TodoProvider";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<TodoProvider>
<App />
</TodoProvider>
);