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를 사용하기 위해서는 다음과 같은 세 가지 주요 요소를 이해해야 합니다.

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

const ThemeContext = React.createContext('light'); // 'light'를 기본값으로 설정
export default ThemeContext;
<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>
  );
}
useContext(Context) Hook: Context 값 사용
  • 함수형 컴포넌트에서 Context 값을 쉽게 구독(subscribe)하고 사용할 수 있는 React Hook입니다.
  • useContext() 훅은 인자로 Context 객체를 받아서, 해당 Context의 현재 value를 반환합니다.
  • 컴포넌트가 가장 가까운 Provider에 의해 제공되는 value를 받습니다. Provider가 없다면 createContext()에 전달된 기본값을 받습니다.
  • Context value가 변경되면, useContext를 사용하는 모든 컴포넌트가 자동으로 다시 렌더링됩니다.
src/MyComponent.js
// 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
// 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;

createContext에서 만든 값을 Provider가 공급하고 useContext로 필요한 컴포넌트만 구독하는 흐름을 아래 구조도로 다시 정리해 보겠습니다.


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

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

Context 정의

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
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
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
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
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
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를 함께 사용하여 좀 더 복잡한 전역 상태를 관리하는 방법을 알아보겠습니다.

목차