Updated:

Context 생성 및 사용


import { createContext, useContext, useState } from 'react';

// 1. Context 생성
const ThemeContext = createContext();

// 2. Provider 컴포넌트 생성
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  // Provider를 통해 값 제공
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. 커스텀 Hook으로 Context 사용 간소화
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// 4. Context 사용하는 컴포넌트
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        border: `1px solid ${theme === 'light' ? '#333' : '#fff'}`,
        padding: '8px 16px',
        borderRadius: '4px'
      }}
    >
      Toggle Theme
    </button>
  );
}

// 5. 앱에서 Provider 사용
function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <h1>Context API Example</h1>
        <ThemedButton />
        <ThemedContent />
      </div>
    </ThemeProvider>
  );
}

function ThemedContent() {
  const { theme } = useTheme();
  
  return (
    <div
      style={{
        backgroundColor: theme === 'light' ? '#f8f9fa' : '#343a40',
        color: theme === 'light' ? '#343a40' : '#f8f9fa',
        padding: '20px',
        margin: '20px 0',
        borderRadius: '8px'
      }}
    >
      <h2>Current Theme: {theme}</h2>
      <p>This content changes based on the current theme.</p>
    </div>
  );
}

코멘트: Context API는 props drilling 없이 컴포넌트 트리 전체에 데이터를 전달하는 방법을 제공합니다. 주요 구성 요소는 다음과 같습니다:

  1. createContext: Context 객체 생성
  2. Context.Provider: 자식 컴포넌트에 값 제공
  3. useContext: 컴포넌트에서 Context 값 사용

커스텀 Hook을 만들어 Context 사용을 간소화하는 것이 좋은 패턴입니다. 이렇게 하면 Context 사용 로직을 캡슐화하고 오류 처리를 추가할 수 있습니다.

여러 Context 조합하기

import { createContext, useContext, useState } from 'react';

// 여러 Context 생성
const ThemeContext = createContext();
const UserContext = createContext();
const LanguageContext = createContext();

// 각 Provider 컴포넌트
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
  
  return (
    <ThemeContext.Provider value=>
      {children}
    </ThemeContext.Provider>
  );
}

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);
  
  return (
    <UserContext.Provider value=>
      {children}
    </UserContext.Provider>
  );
}

function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('en');
  
  const changeLanguage = (lang) => setLanguage(lang);
  
  return (
    <LanguageContext.Provider value=>
      {children}
    </LanguageContext.Provider>
  );
}

// 커스텀 Hooks
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function useUser() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

function useLanguage() {
  const context = useContext(LanguageContext);
  if (context === undefined) {
    throw new Error('useLanguage must be used within a LanguageProvider');
  }
  return context;
}

// 모든 Provider를 결합한 컴포넌트
function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <UserProvider>
        <LanguageProvider>
          {children}
        </LanguageProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// 앱에서 사용
function App() {
  return (
    <AppProviders>
      <Dashboard />
    </AppProviders>
  );
}

// 여러 Context를 사용하는 컴포넌트
function Dashboard() {
  const { theme, toggleTheme } = useTheme();
  const { user, logout } = useUser();
  const { language, changeLanguage } = useLanguage();
  
  return (
    <div className={`dashboard ${theme}`}>
      <header>
        <h1>
          {language === 'en' ? 'Dashboard' : '대시보드'}
        </h1>
        <div className="controls">
          <button onClick={toggleTheme}>
            {language === 'en' ? 'Toggle Theme' : '테마 변경'}
          </button>
          <select 
            value={language} 
            onChange={(e) => changeLanguage(e.target.value)}
          >
            <option value="en">English</option>
            <option value="ko">한국어</option>
          </select>
          {user && (
            <button onClick={logout}>
              {language === 'en' ? 'Logout' : '로그아웃'}
            </button>
          )}
        </div>
      </header>
      <main>
        {user ? (
          <p>
            {language === 'en' 
              ? `Welcome, ${user.name}!` 
              : `환영합니다, ${user.name}님!`}
          </p>
        ) : (
          <LoginForm />
        )}
      </main>
    </div>
  );
}

function LoginForm() {
  const { login } = useUser();
  const { language } = useLanguage();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    // 실제로는 API 호출 등을 통해 인증
    login({ id: 1, name: 'John Doe' });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>{language === 'en' ? 'Login' : '로그인'}</h2>
      <button type="submit">
        {language === 'en' ? 'Sign In' : '로그인'}
      </button>
    </form>
  );
}

코멘트: 여러 Context를 조합할 때는 각 Context를 별도의 Provider로 분리하고, 이를 결합하는 컴포넌트(AppProviders)를 만드는 것이 좋습니다. 이렇게 하면 관심사를 분리하고 코드를 모듈화할 수 있습니다. 각 Context에 대한 커스텀 Hook을 만들면 사용이 더 간편해집니다.

Context 성능 최적화

import { createContext, useContext, useState, useMemo, useCallback } from 'react';

// Context 생성
const CounterContext = createContext();

function CounterProvider({ children }) {
  const [count, setCount] = useState(0);
  
  // useCallback으로 함수 메모이제이션
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);
  
  const decrement = useCallback(() => {
    setCount(c => c - 1);
  }, []);
  
  const reset = useCallback(() => {
    setCount(0);
  }, []);
  
  // useMemo로 Context 값 메모이제이션
  const value = useMemo(() => ({
    count,
    increment,
    decrement,
    reset
  }), [count, increment, decrement, reset]);
  
  return (
    <CounterContext.Provider value={value}>
      {children}
    </CounterContext.Provider>
  );
}

// 커스텀 Hook
function useCounter() {
  const context = useContext(CounterContext);
  if (context === undefined) {
    throw new Error('useCounter must be used within a CounterProvider');
  }
  return context;
}

// Context 값 일부만 사용하는 컴포넌트
function CountDisplay() {
  // count만 사용하고 함수는 사용하지 않음
  const { count } = useCounter();
  
  console.log('CountDisplay rendered');
  
  return <div>Count: {count}</div>;
}

// 다른 Context 값을 사용하는 컴포넌트
function CountButtons() {
  // 함수만 사용하고 count는 사용하지 않음
  const { increment, decrement, reset } = useCounter();
  
  console.log('CountButtons rendered');
  
  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Context 분할 예시
const CountStateContext = createContext();
const CountActionsContext = createContext();

function OptimizedCounterProvider({ children }) {
  const [count, setCount] = useState(0);
  
  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  const reset = useCallback(() => setCount(0), []);
  
  const actions = useMemo(() => ({
    increment, decrement, reset
  }), [increment, decrement, reset]);
  
  return (
    <CountStateContext.Provider value={count}>
      <CountActionsContext.Provider value={actions}>
        {children}
      </CountActionsContext.Provider>
    </CountStateContext.Provider>
  );
}

function useCountState() {
  const context = useContext(CountStateContext);
  if (context === undefined) {
    throw new Error('useCountState must be used within a OptimizedCounterProvider');
  }
  return context;
}

function useCountActions() {
  const context = useContext(CountActionsContext);
  if (context === undefined) {
    throw new Error('useCountActions must be used within a OptimizedCounterProvider');
  }
  return context;
}

// 최적화된 컴포넌트
function OptimizedCountDisplay() {
  // 상태만 구독
  const count = useCountState();
  
  console.log('OptimizedCountDisplay rendered');
  
  return <div>Count: {count}</div>;
}

function OptimizedCountButtons() {
  // 액션만 구독
  const { increment, decrement, reset } = useCountActions();
  
  console.log('OptimizedCountButtons rendered');
  
  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

코멘트: Context를 사용할 때 성능 최적화를 위한 몇 가지 전략이 있습니다:

  1. useMemouseCallback을 사용하여 Context 값과 함수를 메모이제이션
  2. Context를 분할하여 상태와 액션을 별도로 관리 (Context 분할 패턴)
  3. 컴포넌트가 필요한 Context 값만 구독하도록 설계

Context 값이 자주 변경되는 경우, 모든 소비자 컴포넌트가 리렌더링될 수 있으므로 이러한 최적화가 중요합니다. 특히 큰 애플리케이션에서는 Context를 논리적 단위로 분할하는 것이 좋습니다.

Categories:

Updated:

Leave a comment