우리가 큰 건물을 짓는다고 상상해 봅시다. 건물 전체의 ‘중앙 난방 온도’나 ‘비상벨 작동 여부’ 같은 정보는 1층 로비, 10층 사무실, 지하 주차장 등 건물의 모든 곳에서 알고 있어야 합니다. 만약 이 정보를 각 층마다 일일이 전달해야 한다면 얼마나 번거로울까요?
React 애플리케이션 개발도 마찬가지입니다. ‘사용자의 로그인 정보’, ‘현재 선택된 언어’, ‘다크 모드/라이트 모드’처럼 앱의 여러 컴포넌트가 공통으로 알아야 하는 상태가 있습니다. 이런 상태를 전역 상태(Global State)라고 부릅니다.
이번 글에서는 이 전역 상태를 어떻게 효율적으로 관리하는지, 그리고 React에 내장된 강력한 도구인 Context API를 사용하여 ‘Props Drilling(프로퍼티 내려꽂기)’의 고통에서 벗어나 앱 전체의 테마를 관리하는 방법을 단계별로 알아보겠습니다.
📚 주요 개념 설명 (초보자 눈높이)
전역 상태 관리의 세계로 떠나기 전, 핵심 개념들을 쉽게 이해해 봅시다.
- 전역 상태 (Global State): “건물 전체 중앙 방송 시스템”과 같습니다. 특정 컴포넌트 하나에 속한 것이 아니라, 앱의 어느 곳에서든 접근하고 사용할 수 있는 공용 상태 데이터입니다.
- Context API: “중앙 방송 시스템을 만드는 도구”입니다. React가 기본으로 제공하는 기능으로, 데이터를 깊숙한 자식 컴포넌트까지 일일이 props로 넘겨주는 고통(
Props Drilling
) 없이, 원하는 컴포넌트가 ‘방송’을 바로 수신할 수 있게 해줍니다. - 상태 분리 (State Colocation): “주방용품은 주방에, 욕실용품은 욕실에” 두는 것과 같습니다. 모든 상태를 무조건 전역으로 만드는 것은 좋지 않습니다. 특정 컴포넌트 그룹에서만 사용되는 상태는 그 그룹 안에서만 관리하고, 정말로 앱 전체에서 필요한 상태만 전역으로 관리하여 관심사를 분리하는 것이 중요합니다.
- 성능 최적화 (Performance Optimization): 전역 상태가 변경되면, 그 상태를 ‘수신’하는 모든 컴포넌트가 다시 렌더링될 수 있습니다. 불필요한 재렌더링을 막기 위해
useCallback
이나useMemo
같은 기술을 적절히 사용하여 성능을 최적화해야 합니다.
💡 실제 사용 예제: Context API를 활용한 앱 전체 테마 관리
이제 Context API를 사용하여 앱 전체의 라이트/다크 모드를 전환하는 기능을 만들어 보겠습니다.
1단계: 테마 방송국(Context)과 방송 송출 장비(Provider) 만들기
가장 먼저, 테마 정보를 담을 ‘방송국(Context)’을 만들고, 이 방송을 앱 전체에 ‘송출’할 Provider 컴포넌트를 설계합니다.
// contexts/ThemeContext.tsx
'use client';
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { ThemeProvider as MuiThemeProvider, createTheme } from '@mui/material';
// 1. 방송으로 내보낼 데이터의 타입(형식)을 정의합니다.
interface ThemeContextType {
mode: 'light' | 'dark'; // 현재 모드
toggleTheme: () => void; // 모드를 전환할 스위치 함수
}
// 2. 'ThemeContext'라는 이름의 방송국(Context)을 만듭니다.
const ThemeContext = createContext<ThemeContextType | null>(null);
// 3. 방송 송출 장비(Provider) 컴포넌트를 만듭니다.
// 이 컴포넌트가 감싸는 모든 자식들은 방송을 수신할 수 있게 됩니다.
export function ThemeProvider({ children }: { children: ReactNode }) {
const [mode, setMode] = useState<'light' | 'dark'>('light');
// toggleTheme 함수가 불필요하게 계속 새로 만들어지는 것을 방지 (성능 최적화)
const toggleTheme = useCallback(() => {
setMode(prevMode => (prevMode === 'light' ? 'dark' : 'light'));
}, []);
// 현재 모드에 따라 Material-UI 테마 객체를 동적으로 생성
const theme = createTheme({
palette: {
mode,
// ... (라이트/다크 모드에 따른 색상 설정)
},
});
// 4. Provider를 통해 '방송'을 시작합니다!
// value prop으로 실제 데이터(mode와 toggleTheme 함수)를 내려보냅니다.
return (
<ThemeContext.Provider value={{ mode, toggleTheme }}>
{/* Material-UI에게도 우리 테마를 적용해달라고 알려줍니다. */}
<MuiThemeProvider theme={theme}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
);
}
// 5. 방송을 쉽게 수신하기 위한 '수신기'(커스텀 훅) 만들기
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
// 혹시 방송국 밖에서 수신기를 켜려고 하면 에러 발생!
throw new Error('useTheme은 ThemeProvider 안에서만 사용할 수 있습니다.');
}
return context;
}
2단계: 앱 전체에 방송 송출 장비(Provider) 설치하기
가장 상위 레이아웃 파일에서 우리 앱 전체를 ThemeProvider
로 감싸줍니다. 이제 이 앱의 모든 컴포넌트는 ‘테마 방송’을 수신할 준비가 되었습니다.
// app/layout.tsx
import { ThemeProvider } from '@/contexts/ThemeContext';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<ThemeProvider> {/* 앱 전체를 감싸주기 */}
{children}
</ThemeProvider>
</body>
</html>
);
}
3단계: 필요한 곳에서 ‘수신기'(useTheme 훅) 켜기
이제 테마 정보가 필요한 어떤 컴포넌트에서든, 우리가 만든 useTheme
훅을 호출하기만 하면 됩니다.
// components/DataGrid/ThemedDataGrid.tsx
'use client';
import { useTheme } from '@/contexts/ThemeContext'; // 우리의 수신기!
import { Paper, Box, Typography, IconButton } from '@mui/material';
import { Brightness4 as DarkIcon, Brightness7 as LightIcon } from '@mui/icons-material';
export function ThemedDataGrid() {
// 수신기를 켜서 방송 데이터를 받습니다!
const { mode, toggleTheme } = useTheme();
return (
<Paper elevation={3} sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="h6">테마 적용 데이터 그리드</Typography>
{/* 테마 전환 버튼: 클릭하면 toggleTheme 함수가 실행됨 */}
<IconButton onClick={toggleTheme} color="inherit">
{mode === 'dark' ? <LightIcon /> : <DarkIcon />}
</IconButton>
</Box>
{/* ... (데이터 그리드 내용) ... */}
</Paper>
);
}
🎨 테마 관리 예제 설명
이 예제를 통해 우리는 무엇을 배울 수 있을까요?
- 목적과 기능: 앱 전체에 일관된 디자인 테마(라이트/다크 모드)를 적용하고, 사용자가 언제든지 이 테마를 쉽게 전환할 수 있도록 하는 것이 목표입니다.
- Props Drilling 문제 해결:
useTheme
훅 덕분에ThemedDataGrid
컴포넌트는 자신의 부모가 누구인지, 얼마나 많은 컴포넌트를 거쳐왔는지 전혀 신경 쓸 필요가 없습니다. 그저 ‘테마 방송’을 수신하기만 하면 됩니다. 이것이 바로 Context API가Props Drilling
문제를 해결하는 방식입니다. - 구조의 명확성:
ThemeContext.tsx
: 상태와 로직을 한 곳에 모아 관리하여 ‘방송국’의 역할을 명확히 합니다.layout.tsx
: 앱 전체에 기능을 제공하는Provider
를 어디에 위치시켜야 하는지 보여줍니다.ThemedDataGrid.tsx
:Provider
가 제공하는 데이터를 소비하는 ‘소비자’의 역할을 보여줍니다.
- 커스텀 훅의 편리함:
useTheme
과 같은 커스텀 훅을 만들어두면,useContext(ThemeContext)
코드를 반복적으로 작성할 필요가 없고, Provider가 없는 곳에서 잘못 사용하는 실수를 미연에 방지할 수 있어 코드의 안정성과 재사용성이 높아집니다.