icon
7장 : 상태 관리 입문

Context API 기초

리액트 애플리케이션에서 여러 컴포넌트가 상태를 공유할 때 발생하는 프롭스 드릴링(Props Drilling) 의 문제점과, 이를 해결하기 위한 상태 관리 솔루션의 필요성에 대해 알아보았습니다. 이제 7장 "상태 관리 입문"의 두 번째 절에서는 리액트 자체적으로 제공하는 강력한 상태 관리 도구인 Context API에 대해 깊이 있게 알아보겠습니다.

Context API는 프롭스 드릴링 없이 컴포넌트 트리의 깊이에 상관없이 데이터를 전달하고 공유할 수 있게 해주는 메커니즘입니다. 복잡한 외부 라이브러리 없이도 전역 상태와 유사한 기능을 구현할 수 있어, 중간 규모의 애플리케이션에서 매우 유용하게 사용될 수 있습니다.


Context API란?

React Context API는 컴포넌트 트리를 통해 데이터를 "내려주는(passing down)" 방식 없이도 컴포넌트 간에 데이터를 공유할 수 있는 방법을 제공합니다. 즉, 프롭스를 일일이 전달하지 않고도, 특정 종류의 값을 컴포넌트 트리의 여러 레벨에 걸쳐 "제공(provide)"할 수 있게 해줍니다.

주요 사용 사례

  • 테마(Theme) 설정: 다크 모드/라이트 모드와 같이 애플리케이션 전반에 걸쳐 적용되는 UI 스타일.
  • 사용자 인증(Authentication) 정보: 로그인한 사용자 정보, 로그인 상태 등.
  • 언어 설정: 다국어 지원 시 현재 언어 설정.

이러한 "전역적"이거나 "준-전역적"인 데이터를 전달하는 데 특히 효과적입니다.


Context API의 주요 요소

Context API를 사용하기 위해서는 다음과 같은 세 가지 주요 요소를 이해해야 합니다.

  1. React.createContext(): Context 객체 생성

    • Context를 사용하기 위한 가장 첫 단계입니다.
    • createContext() 함수를 호출하여 Context 객체를 생성합니다. 이 객체는 ProviderConsumer (또는 useContext 훅) 컴포넌트를 포함합니다.
    • 인자로는 Context의 기본값(default value) 을 받습니다. 이 기본값은 Context를 제공하는 Provider가 없을 때, 또는 Provider에서 제공하는 값이 undefined일 때 사용됩니다.
    src/contexts/ThemeContext.js
    // src/contexts/ThemeContext.js
    import React from 'react';
    
    const ThemeContext = React.createContext('light'); // 'light'를 기본값으로 설정
    export default ThemeContext;
  2. <Context.Provider>: Context 값 제공

    • Context 객체에 포함된 Provider 컴포넌트는 Context를 제공할 컴포넌트 트리의 최상단에 위치합니다.
    • value prop을 통해 자식 컴포넌트들에게 전달할 실제 데이터를 명시합니다. 이 value는 어떤 타입(문자열, 숫자, 객체, 함수 등)이든 될 수 있습니다.
    • Provider는 중첩될 수 있으며, 가장 가까운 Providervalue가 하위 컴포넌트에 전달됩니다.
    // src/App.js (ThemeContext.js가 있다고 가정)
    import React, { useState } from 'react';
    import ThemeContext from './contexts/ThemeContext'; // Context 임포트
    import MyComponent from './MyComponent'; // Context 값을 사용할 컴포넌트
    
    function App() {
      const [theme, setTheme] = useState('light'); // 테마 상태
    
      const toggleTheme = () => {
        setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
      };
    
      return (
        // ThemeContext.Provider로 하위 컴포넌트들을 감싸고 value를 제공
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
          <div style={{ padding: '20px' }}>
            <h1>Context API 예제</h1>
            <p>현재 앱 테마: {theme}</p>
            <MyComponent /> {/* MyComponent 및 그 하위에서 theme, toggleTheme 접근 가능 */}
          </div>
        </ThemeContext.Provider>
      );
    }
  3. useContext(Context) Hook: Context 값 사용

    • 함수형 컴포넌트에서 Context 값을 쉽게 구독(subscribe)하고 사용할 수 있는 React Hook입니다.
    • useContext() 훅은 인자로 Context 객체를 받아서, 해당 Context의 현재 value를 반환합니다.
    • 컴포넌트가 가장 가까운 Provider에 의해 제공되는 value를 받습니다. Provider가 없다면 createContext()에 전달된 기본값을 받습니다.
    • Context value가 변경되면, useContext를 사용하는 모든 컴포넌트가 자동으로 다시 렌더링됩니다.
    // src/MyComponent.js
    import React, { useContext } from 'react';
    import ThemeContext from './contexts/ThemeContext'; // Context 임포트
    import DeeplyNestedComponent from './DeeplyNestedComponent';
    
    function MyComponent() {
      // useContext 훅을 사용하여 ThemeContext의 현재 값(theme, toggleTheme)을 가져옴
      const { theme, toggleTheme } = useContext(ThemeContext);
    
      return (
        <div style={{ padding: '15px', border: `1px solid ${theme === 'light' ? '#ccc' : '#555'}`, marginTop: '20px' }}>
          <h3>MyComponent</h3>
          <p>현재 테마 (MyComponent): {theme}</p>
          <button onClick={toggleTheme}>테마 전환 (MyComponent)</button>
          <DeeplyNestedComponent />
        </div>
      );
    }
    
    export default MyComponent;
    
    // src/DeeplyNestedComponent.js
    import React, { useContext } from 'react';
    import ThemeContext from './contexts/ThemeContext';
    
    function DeeplyNestedComponent() {
      const { theme } = useContext(ThemeContext); // 테마 값만 필요하면 theme만 가져옴
    
      return (
        <div style={{ marginTop: '15px', padding: '10px', backgroundColor: theme === 'light' ? '#f0f0f0' : '#444', color: theme === 'light' ? '#333' : '#ddd', borderRadius: '5px' }}>
          <h4>깊게 중첩된 컴포넌트</h4>
          <p>이 컴포넌트도 프롭스 없이 테마를 받았습니다: {theme}</p>
        </div>
      );
    }
    
    export default DeeplyNestedComponent;

Context API를 이용한 테마 전환 예제 (통합)

이전 장에서 프롭스 드릴링의 예시로 들었던 테마 전환 기능을 Context API로 재구현해 보겠습니다.

Context 정의

src/contexts/ThemeContext.js
// src/contexts/ThemeContext.js
import React from 'react';

// Context를 생성하고 기본값을 설정합니다.
// 기본값은 Provider가 없을 때 사용되거나, 개발 도구에서 타입 힌트를 제공하는 데 사용됩니다.
const ThemeContext = React.createContext({
  theme: 'light', // 기본 테마
  toggleTheme: () => {}, // 기본 함수 (아무것도 하지 않음)
});

export default ThemeContext;

Context Provider를 사용한 컴포넌트

src/App.js
// src/App.js
import React, { useState } from 'react';
import ThemeContext from './contexts/ThemeContext'; // Context 임포트
import Header from './components/Header';
import MainContent from './components/MainContent';
import Footer from './components/Footer';

function App() {
  const [theme, setTheme] = useState('light'); // 테마 상태 관리

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  // Context.Provider의 value prop을 통해 { theme, toggleTheme } 객체를 하위 컴포넌트에 제공
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
        <h1>Context API 예제</h1>
        <p>최상위 앱 컴포넌트의 테마: {theme}</p>
        <Header />
        <MainContent />
        <Footer />
      </div>
    </ThemeContext.Provider>
  );
}

export default App;

Context 값을 사용하는 컴포넌트들

src/components/Header.js
// src/components/Header.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext'; // Context 임포트

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext); // useContext 훅으로 Context 값 접근

  return (
    <header style={{
      backgroundColor: theme === 'light' ? '#eee' : '#333',
      color: theme === 'light' ? '#333' : '#eee',
      padding: '15px',
      borderRadius: '5px',
      marginBottom: '20px'
    }}>
      <h2>헤더</h2>
      <p>현재 테마 (헤더): {theme}</p>
      <button onClick={toggleTheme}>테마 전환 (헤더)</button> {/* 함수도 사용 가능 */}
    </header>
  );
}

export default Header;
src/components/MainContent.js
// src/components/MainContent.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext';
import ContentSection from './ContentSection'; // 더 깊은 자식 컴포넌트

function MainContent() {
  const { theme } = useContext(ThemeContext); // theme 값만 필요

  return (
    <div style={{
      backgroundColor: theme === 'light' ? '#f8f8f8' : '#555',
      color: theme === 'light' ? '#333' : '#eee',
      padding: '20px',
      borderRadius: '5px',
      marginBottom: '20px'
    }}>
      <h3>메인 콘텐츠</h3>
      <p>이곳은 주요 내용이 표시되는 공간입니다.</p>
      <ContentSection /> {/* props 전달 없이 바로 사용 */}
    </div>
  );
}

export default MainContent;
src/components/ContentSection.js
// src/components/ContentSection.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext';

function ContentSection() {
  const { theme } = useContext(ThemeContext); // theme 값만 필요

  return (
    <div style={{
      border: `1px solid ${theme === 'light' ? '#ccc' : '#888'}`,
      padding: '15px',
      borderRadius: '5px',
      marginTop: '15px'
    }}>
      <h4>콘텐츠 섹션</h4>
      <p>이 텍스트의 색깔도 테마에 따라 변합니다. 프롭스 드릴링 없이!</p>
      <p style={{ color: theme === 'light' ? 'blue' : 'lightblue' }}>현재 테마: {theme}</p>
    </div>
  );
}

export default ContentSection;
src/components/Footer.js
// src/components/Footer.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext';

function Footer() {
  const { theme } = useContext(ThemeContext); // theme 값만 필요

  return (
    <footer style={{
      backgroundColor: theme === 'light' ? '#eee' : '#333',
      color: theme === 'light' ? '#333' : '#eee',
      padding: '15px',
      borderRadius: '5px'
    }}>
      <p>&copy; 2024 Context API 앱. 현재 테마: {theme}</p>
    </footer>
  );
}

export default Footer;

Context API의 장점 및 고려 사항

장점

  • 프롭스 드릴링 해결: 중간 컴포넌트를 거치지 않고 직접적으로 필요한 데이터에 접근할 수 있습니다.
  • 코드 간결성: props를 계속해서 전달하는 코드가 사라져 가독성이 향상됩니다.
  • 리액트 내장 기능: 별도의 라이브러리 설치 없이 리액트 기본 기능으로 사용 가능합니다.
  • 재사용성 증가: 전역적으로 필요한 상태를 중앙에서 관리하고 여러 컴포넌트에서 재사용하기 용이합니다.

고려 사항

  • 재렌더링 문제: Context value가 변경될 때마다 해당 Context를 useContext로 구독하고 있는 모든 하위 컴포넌트가 불필요하게 재렌더링될 수 있습니다. 만약 Context value가 자주 변경되거나, 그 value에 여러 가지 상태(객체, 함수)가 복합적으로 포함되어 있다면 성능 문제가 발생할 수 있습니다.
    • 이를 완화하기 위해 React.memo, useCallback, useMemo와 같은 메모이제이션 기법을 함께 사용할 수 있습니다.
    • Context를 여러 개로 분리하여 특정 상태에만 관심 있는 컴포넌트가 해당 Context만 구독하도록 할 수도 있습니다.
  • 단일 책임 원칙: Context는 "전역적인" 데이터를 전달하기 위한 것이지, 복잡한 비즈니스 로직이나 매우 자주 업데이트되는 상태를 관리하기 위한 완전한 상태 관리 솔루션은 아닙니다. 너무 많은 상태를 하나의 Context에 넣으면 관리하기 어려워집니다.
  • 디버깅의 어려움: props처럼 명시적으로 전달되지 않기 때문에, 어떤 컴포넌트가 어떤 Context 값을 사용하고 있는지 파악하기 어려울 수 있습니다. React 개발자 도구의 Components 탭에서 Context 값을 확인할 수 있습니다.

언제 Context API를 사용할까?

  • 애플리케이션 전반에 걸쳐 자주 사용되지만, 빈번하게 업데이트되지 않는 데이터: 테마, 사용자 인증 정보, 언어 설정 등.
  • 중간 규모의 애플리케이션: 복잡한 외부 상태 관리 라이브러리(Redux, Zustand 등)를 도입하기에는 오버헤드가 크다고 판단될 때.
  • 컴포넌트 트리가 깊어 프롭스 드릴링이 심각할 때.

"Context API 기초"는 여기까지입니다. 이 장에서는 Context API의 개념, createContext(), <Context.Provider>, useContext() 훅이라는 세 가지 핵심 요소를 자세히 살펴보고, 실제 테마 전환 예제를 통해 Context API의 강력함을 경험했습니다. 또한 Context API의 장점과 함께 고려해야 할 재렌더링 문제 등도 짚어보았습니다.

이제 여러분은 리액트 애플리케이션에서 프롭스 드릴링 없이 전역적인 상태를 효율적으로 공유하는 방법을 이해하게 되었습니다. 다음 장에서는 useReducer 훅과 Context API를 함께 사용하여 좀 더 복잡한 전역 상태를 관리하는 방법을 알아보겠습니다.