[React] React Context API

Featured image

일반적으로 React 에서 데이터는 부모로부터 자식에게 props를 통해 전달된다. 그러나 개발 중 어플리케이션 안에서 여러 Component 들에 데이터를 전달해야 할 경우 이 과정이 복잡하고 비효율적일 수 있다. 이와 같은 문제를 해결하기 위해 React 에서 Context API 를 제공하게 되었다.

Context 를 사용하면 단계마다 props 를 넘겨주지 않고도 Component 전체에 데이터를 제공할 수 있다. 즉, Context 는 Component 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법이다. 주로 Context 가 필요한 데이터로는 현재 로그인한 유저정보, 테마, 언어 정보 등이 있을 수 있다.

Context 의 주된 용도는 다양한 레벨의 Component 에게 데이터를 전달하는 것이다. context 를 사용하면 컴포넌트 재사용이 어려워 질 수 있기 때문에 꼭 필요할때만 사용하는 것이 좋다.

Context API 사용법

React.createContext

const MyContext = React.createContext(defaultValue);

Context 객체를 만들기 위해 createContext 라는 함수를 사용한다. createContext 함수의 인자로 해당 Context 에 default 로 저장할 값을 넘긴다.

Context 객체를 구독하고 있는 Component 를 렌더링할 때, React 는 트리 상위에서 가장 가까이 있는 Provider 로 부터 현재 값을 읽는다.

defaultValue 매개변수는 Component 트리 안에서 적절한 Provider 를 찾지 못했을 경우에만 쓰이는 값이다. (Provider 를 통해 undefined 값을 보내도 구독 Component 들이 defaultValue 를 사용하지는 않는다.)

React.Provider

<MyContext.Provider value={/* 어떤 값 */}>

Provider 는 context 를 구독하는 Component 들에게 context 의 변화를 알리는 역할을 한다.

Provider Component 는 value prop 을 하위의 Component 들에게 전달하고, 전달 받을 수 있는 Component 의 수에 제한이 없다. Provider 하위에 다른 Provider를 배치하는 것도 가능하고, 이때 하위의 Provider 값이 우선시된다.

Provider 하위에서 context를 구독하는 component 는 Provider 의 value prop이 바뀔 때마다 다시 re-rendering 된다.

이제 생성하고, 저장한 Context 에 접근할 수 있는 3가지 방법을 알아보자.

Class.contextType

먼저 React.createContext() 로 생성한 Context 객체를 원하는 클래스의 contextType property로 지정할 수 있다. contextType 를 활용하면, 클래스 안에서 this.context 를 사용하여 해당 Context 의 가장 가까운 Provider 를 찾아 그 값을 읽을 수 있게 된다.

import React, { Component } from "react";
import MyContext from "./MyContext";

class ClassComponentTest extends Component {
  static contextType = MyContext;

  componentDidMount() {
    console.log(this.context);
  }

  render() {
    let value = this.context;
    /* context 값을 이용한 렌더링 */
  }
}

contextType 는 Class Component 에서만 사용이 가능하다. 또한 contextType 을 사용하면 하나의 context 만 구독 할 수 있기 때문에 여러 context 를 구독하기 위해서는 Consumer 를 사용해야한다.

Context.Consumer

Context 에 접근하는 두 번째 방법으로는 Consumer 가 있다. Consumer 는 context 변화를 구독하는 React Component 이다. Consumer 를 사용하면 함수형 Component 안에서 context 를 구독할 수 있다.

<MyContext.Consumer>
  {value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>

Context.Consumer 의 자식은 함수(Component) 이어야 한다. 이때 이 함수는 context 의 현재 값을 받고 React 노드를 반환한다. 함수가 받는 value 매개변수 값은 해당 context 의 Provider 중 상위 트리에서 가장 가까운 Provider 의 value prop 과 동일하다. (상위에 Provider가 없다면 value 매개변수 값은 createContext()에 보냈던 defaultValue와 동일할 것 이다.)

useContext()

const value = useContext(MyContext);

React Hooks 에서 추가된 useContext 를 사용해서 context 에 접근할 수 있다. useContext 를 사용하여 context 객체를 받아 그 context 의 현재 값을 반환한다. 이 방법은 함수형 Component 에서만 사용 가능하다.

예제

이제 Context API 를 사용하여 theme 값이 변하는 어플리케이션을 만들어보자.

먼저 theme 에 대한 정보를 담고 있는 전역 Context 객체를 생성한다.

ThemeContext.jsx
export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // 기본값
);

이때 dark 객체를 defaultValue 로 지정한다.

ThemedButton.jsx

context 를 접근하여 사용할 ThemedButton Component 를 먼저 생성해보자.

첫 번재로 contextType 방식으로 Context 에 접근을 한다면 아래와 같다.

import {ThemeContext} from './ThemeContext';

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  
  render() {
    let theme = this.context;
    return (
      <button
        style={backgroundColor: theme.background}
      />
    );
  }
}

두 번째로 Consumer 방식으로 Context 에 접근을 한다면 아래와 같다.

import {ThemeContext} from './ThemeContext';

function ThemedButton() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button
          style={backgroundColor: theme.background}
        />
      )}
    </ThemeContext.Consumer>
  );
  
}

세 번째로 useContext 함수를 사용하여 Context 에 접근한다면 조금 더 깔끔한 코드가 될 수 있다.

import React, { useContext } from "react";
import ThemeContext from "./ThemeContext";

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (<button style={backgroundColor: theme.background} />);
}
app.jsx

이제 Provider 로 하위 Compoent(ThemedButton) 을 감싸주어 Context 에 접근 가능하도록 설정한다.

import {ThemeContext, themes} from './ThemeContext';
import ThemedButton from './ThemedButton';

// ThemedButton를 사용하는 중간에 있는 컴포넌트
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

  render() {
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

Provider 로 Toolbar 를 감싸주었으므로, Toolbar Component 에서는 value 로 가진 state theme 값을 읽는다. 그러나 그 밖의 Section 안에 있는 ThemedButton 에서는 defaultValue로 설정했던 dark 를 사용하게 된다.

또한 Provider 하위 Component Toolbar 는 state 가 변화할 때 마다 re-rendering 하게 된다.

하위 Component 에서 Context 업데이트 하기

Component 하위 에서 context 를 업데이트 해야할 경우에는 context 를 통해 함수를 보내면 된다.

export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
import {ThemeContext} from './ThemeContext';

function ThemeTogglerButton() {
  // ThemeTogglerButton는 context로부터
  // theme 값과 함께 toggleTheme 매서드도 받고 있습니다.
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={backgroundColor: theme.background}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;
import {ThemeContext, themes} from './ThemeContext';
import ThemeTogglerButton from './ThemeTogglerButton';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // state에 업데이트 메서드도 포함되어있으므로
    // 이 또한 context Provider를 통해 전달될것입니다.
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

Reference