Prop Drilling 문제
flowchart TB
APP["App\nuser={user}"]
LAYOUT["Layout\nuser={user}"]
SIDEBAR["Sidebar\nuser={user}"]
AVATAR["Avatar\nuser={user}"]
APP --> LAYOUT --> SIDEBAR --> AVATAR
NOTE["Avatar만 user가 필요한데\n중간 컴포넌트를 모두 거쳐야 함"]
Context를 쓰면 중간 단계 없이 직접 전달할 수 있습니다.
Context 만들기
// src/contexts/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from "react";
interface User {
id: string;
name: string;
email: string;
role: "admin" | "user";
}
interface AuthContextValue {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextValue | null>(null);
// 커스텀 훅으로 컨텍스트 사용 간소화
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth는 AuthProvider 안에서 사용해야 합니다.");
return context;
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
async function login(email: string, password: string) {
setIsLoading(true);
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
setUser(data.user);
} finally {
setIsLoading(false);
}
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
);
}
Provider로 감싸기
// src/main.tsx
import { AuthProvider } from "./contexts/AuthContext";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</StrictMode>
);
어디서나 사용
// 어떤 깊이의 컴포넌트에서든
function UserAvatar() {
const { user } = useAuth();
if (!user) return null;
return <img src={`/avatars/${user.id}`} alt={user.name} />;
}
function LogoutButton() {
const { logout } = useAuth();
return <button onClick={logout}>로그아웃</button>;
}
function AdminPanel() {
const { user } = useAuth();
if (user?.role !== "admin") return <p>접근 권한이 없습니다.</p>;
return <div>관리자 패널</div>;
}
테마 Context 예시
// src/contexts/ThemeContext.tsx
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: "light",
toggleTheme: () => {},
});
export function useTheme() {
return useContext(ThemeContext);
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
const toggleTheme = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 사용
function DarkModeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
{theme === "light" ? "🌙 다크모드" : "☀️ 라이트모드"}
</button>
);
}
Context 성능 고려
// 분리되지 않은 Context — 일부만 바뀌어도 전체 구독자 리렌더링
const UserContext = createContext({ user, settings, notifications });
// ✅ 분리된 Context — 각각 독립 업데이트
const UserContext = createContext(user);
const SettingsContext = createContext(settings);
const NotificationsContext = createContext(notifications);
정리
flowchart LR
PROVIDER["Context.Provider\n값 제공"]
CONSUMER1["컴포넌트 A\nuseContext()"]
CONSUMER2["컴포넌트 B\nuseContext()"]
CONSUMER3["깊은 컴포넌트\nuseContext()"]
PROVIDER --> CONSUMER1
PROVIDER --> CONSUMER2
PROVIDER --> CONSUMER3
| 개념 | 설명 |
|---|---|
createContext | Context 생성 |
Provider | 값을 하위 트리에 제공 |
useContext | Context 값 소비 |
| 커스텀 훅 | useAuth, useTheme 등 사용성 개선 |
다음 편에서는 커스텀 훅 — 로직을 재사용 가능한 훅으로 추출하는 방법을 배웁니다.