Logo

React Context 사용법

리액트 앱을 개발하다보면 모든 컴포넌트에 어떤 값을 props으로 넘기고 싶은 데이터가 필요할 때가 있습니다. 다시 말해, 어떤 컴포넌트 트리 내에서 최상위 컴포넌트 부터 최말단 컴포넌트에 걸쳐 전역(global)으로 관리해야 할 데이터가 필요한 상황이 되겠네요.

이번 포스팅에서는 React Context를 활용하여 리액트 앱에서 전역 데이터를 관리하는 방법에 대해서 알아보도록 하겠습니다.

React Context 없이 개발하기

먼저 React Context 없이 props만을 이용해서 어떻게 전역 데이터를 여러 컴포넌트에 걸쳐서 접근할 수 있는지에 대해서 살펴보도록 하겠습니다. 예를 들어, 다음과 같이 사용자가 설정한 언어에 따라서 영어 또는 한국어로 텍스트를 표시해주는 앱이 있다고 생각해봅시다.

import React, { Component, Fragment } from "react";
import Button from "./Button";
import Title from "./Title";
import Message from "./Message";

class App extends Component {
  state = { lang: "en" };

  toggleLang = () => {
    this.setState(({ lang }) => ({
      lang: lang === "en" ? "kr" : "en",
    }));
  };

  render() {
    const { lang } = this.state;
    return (
      <Fragment>
        <Button lang={lang} toggleLang={this.toggleLang} />
        <Title lang={lang} />
        <Message lang={lang} />
      </Fragment>
    );
  }
}

위 리액트 앱은 Button, Title, Message 이렇게 3개의 컴토넌트로 구성되어 있습니다. 최상위 앱의 state에 저장되어 있는 사용자 언어 설정값 lang은 모든 하위 컴포넌트에 props으로 전달되고 있습니다. Button 컴포넌트에는 추가적으로 toggleLang 함수값도 넘기고 있는데, 이 함수는 사용자 언어를 en또는 kr로 번갈아 가면서 설정하는데 쓰이게 됩니다.

하위 컴포넌트

3개의 하위 컴포넌트는 다음과 같이 props로 넘어온 lang 값에 따라서 영어와 한국어의 텍스트를 렌더링합니다. Button 컴포넌트는 toggleLang 함수값을 onClick 핸들러에 설정해줌으로써 버튼 클릭 시 사용자 언어 설정값이 바뀌도록 합니다. Message 컴포넌트는 클래스로 구현해보았는데, 아래에서 React Context 사용법을 보여주기 위합입니다.

import React, { Component } from "react";

function Button({ lang, toggleLang }) {
  return <button onClick={toggleLang}>{lang}</button>;
}

function Title({ lang }) {
  const text = lang === "en" ? "Context" : "컨텍스트";
  return <h1>{text}</h1>;
}

class Message extends Component {
  render() {
    const { lang } = this.props;
    if (lang === "en")
      return (
        <p>
          "Context provides a way to pass data through the component tree
          without having to pass props down manually at every level"
        </p>
      );
    else
      return (
        <p>
          "컨텍스트는 모든 레벨에서 일일이 props를 넘기지 않고도 컴포넌트 트리에
          걸쳐서 데이터를 전달할 수 있는 방법을 제공합니다."
        </p>
      );
  }
}

문제점

위 코드는 모든 컴포넌트에서 사용자 언어 설정값을 전달하기 위해서 props를 사용하고 있습니다. 예제에서는 컴포넌트 수가 몇개 되지 않게 때문에 크게 번거롭지 않아보이지만, 실제 수십, 수백개의 컴포넌트로 이뤄진 리액트 앱에서는 어떨까요? 특히, 다국어를 지원하지 않던 앱에서 갑자기 다국어를 지원해야하는 상황을 생각해보면 일일이 모든 컴포넌트 props을 추가해주는 게 매우 끔찍할 것입니다.

React Context를 사용해서 개발하기

위에서 살펴본 것 처럼 큰 규모의 앱에서는 전역 데이터를 관리하는데 좀 더 나은 접근 방식이 필요합니다. React Context는 전역 데이터를 좀 더 단순하지만 체계적인 방식으로 접근할 수 있도록 도와줍니다.

지금부터 위에서 작성한 리액트 앱을 React Context를 사용해서 코드 리팩토링 해보면서 그 효과를 살펴보겠습니다.

Context 생성하기

전역 데이터를 관리하기 위해서 React 패키지에서 제공하는 createContext라는 함수를 사용합니다. 개념적으로 React Context는 전역 데이터를 담고 있는 하나의 저장 공간이라고 생각할 수 있습니다.

다음과 같이 createContext 함수의 인자로 해당 컨텍스트에 디폴트로 저장할 값을 넘깁니다.

import { createContext } from "react";

const LangContext = createContext("en");

Context 저장하기

다음과 같이 어떤 컴포넌트에서 Provider로 감싸주면, 그 하위에 있는 모든 컴포넌트들은 이 React Context에 저장되어 있는 전역 데이터에 접근할 수 있습니다. value 속성값을 지정하지 않을 경우, Context를 생성할 때 넘겼던 디폴트값이 사용됩니다.

기존 앱에서는 모든 하위 컴포넌트에 props로 사용자 언어 설정값인 lang을 넘겨줘었는데, 바뀐 코드에서는 대신에 Provider로 한번 감싸고 value로 이 값을 설정해주고 있습니다.

import React, { Component } from "react";
import LangContext from "./LangContext";
import Button from "./Button";
import Title from "./Title";
import Message from "./Message";

class App extends Component {
  state = { lang: "en" };

  toggleLang = () => {
    this.setState(({ lang }) => ({
      lang: lang === "en" ? "kr" : "en",
    }));
  };

  render() {
    const { lang } = this.state;
    return (
      <LangContext.Provider value={lang}>
        <Button toggleLang={this.toggleLang} />
        <Title />
        <Message />
      </LangContext.Provider>
    );
  }
}

자 그럼 이제 상위 컴포넌트에서 Provider를 이용하여 저장한 전역 데이터를 하위 컴포넌트에서 접근할 수 있습니다. 크게 3가지 방법으로 접근을 할 수 있는데 하나씩 알아보겠습니다.

Consumer로 Context 접근하기

먼저 Provider와 대응하는 Consumer를 이용하여 Context에 저장되어 있는 전역 데이터에 접근할 수 있습니다. Consumerrender props을 받기 때문에, Title 컴포넌트는 children으로 넘기는 함수의 인자로 사용자 언어 설정값을 읽습니다.

import React from "react";
import LangContext from "./LangContext";

function Title() {
  return (
    <LangContext.Consumer>
      {(lang) => {
        const text = lang === "en" ? "Context" : "컨텍스트";
        return <h1>{text}</h1>;
      }}
    </LangContext.Consumer>
  );
}

useContext로 Context 접근하기

React Hooks에서 추가된 useContext 함수를 이용하면 좀 더 깔끔하게 Context에 저장되어 있는 전역 데이터에 접근할 수 있습니다. Button 컴포넌트는 useContext 함수에 LangContext를 넘김으로써 사용자 언어 설정값을 읽습니다. 단, 이 방법은 함수 컴포넌트에서만 사용 가능합니다.

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

function Button({ toggleLang }) {
  const lang = useContext(LangContext);
  return <button onClick={toggleLang}>{lang}</button>;
}

contextType으로 Context 접근하기

Message 컴포넌트와 같이 클래스로 구현된 컴포넌트의 경우에는 contextType을 사용해서 Context에 저장되어 있는 전역 데이터에 접근할 수 있습니다. 단, 이 방법은 클래스 컴포넌트에서만 사용 가능합니다.

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

class Message extends Component {
  static contextType = LangContext;

  render() {
    const lang = this.context;
    if (lang === "en")
      return (
        <p>
          "Context provides a way to pass data through the component tree
          without having to pass props down manually at every level"
        </p>
      );
    else
      return (
        <p>
          "컨텍스트는 모든 레벨에서 일일이 props를 넘기지 않고도 컴포넌트 트리에
          걸쳐서 데이터를 전달할 수 있는 방법을 제공합니다."
        </p>
      );
  }
}

주의 사항

모든 기능이 그러하듯 React Context는 꼭 본연의 용도에 맞는 상황에서만 사용해야 합니다. 즉, 전역 데이터를 한 곳에서 저장하고 여러 컴포넌트에서 접근하고 싶은 경우가 아니라 사용을 피해야 합니다. 왜냐하면 React Context를 사용하게 되면 해당 컴포넌트는 해당 Context가 없이는 재사용이 어렵기 때문입니다.

마치면서

이상으로 React Context를 왜 사용하고 어떻게 사용하는지에 대해서 살펴보았습니다. 전체 코드는 아래를 참고 바랍니다.

  • React Context 적용 전: Edit global-lang-without-context
  • React Context 적용 후: Edit global-lang-with-context