React 클라이언트 상태 관리 (전역 상태 관리와 Context API)

1 min read

우리가 큰 건물을 짓는다고 상상해 봅시다. 건물 전체의 ‘중앙 난방 온도’나 ‘비상벨 작동 여부’ 같은 정보는 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가 없는 곳에서 잘못 사용하는 실수를 미연에 방지할 수 있어 코드의 안정성과 재사용성이 높아집니다.

next.js 클라이언트 상태 관리 웹 캐싱 전략의 모든 것…

매번 똑같은 책을 빌리기 위해 도서관에 가는 것과, 자주 보는 책 몇 권은 아예 책상 위에 꺼내두고 보는 것 중 어느 쪽이 더 빠를까요?...
eve
1 min read

 React 19 자동 메모이제이션 (Automatic Memoization)

혹시 개발 공부를 하다가 useMemo, useCallback 같은 훅(Hook)들을 만나고 “이건 왜 써야 하지? 너무 복잡해…”라고 생각했던 적 있으신가요? 이런 훅들은 React의 성능을 최적화하기 위해,...
eve
46 sec read

next.js 오프라인 지원(Offline Support)

비행기나 지하철 안에서, 또는 와이파이가 불안정한 카페에서 갑자기 인터넷이 끊겨 작업하던 내용이 날아가거나 앱이 먹통이 된 경험, 한 번쯤 있으시죠? 만약 우리 앱이 이런...
eve
55 sec read