Context

Context를 사용하면 일일이 props를 내려보내주지 않아도 데이터를 컴포넌트 트리 아래쪽으로 전달할 수 있습니다.

전형적인 React 어플리케이션에서, 데이터는 props를 통해 위에서 아래로 (부모에서 자식으로) 전달됩니다. 하지만 이런 방식은 몇몇 유형의 props에 대해서는 굉장히 번거로운 방식일 수 있습니다. (예를 들어 언어 설정, UI 테마 등) 어플리케이션의 많은 컴포넌트들에서 이를 필요로 하기 때문입니다. Contetxt를 사용하면 prop을 통해 트리의 모든 부분에 직접 값을 넘겨주지 않고도, 값을 공유할 수 있습니다.

역주:

이 문서에서 소개하는 Context API는 2018년 3월 30일에 배포된 React 16.3 버전에서 추가되었습니다.

언제 Context를 사용해야 할까요?

Context는 React 컴포넌트 트리 전체에 걸쳐 데이터를 공유하기 위해 만들어졌습니다. 그러한 데이터로는 로그인 된 사용자의 정보, 테마, 언어 설정 등이 있을 수 있겠죠. 예를 들어, 아래 코드에서는 Button 컴포넌트의 스타일링을 위해 “theme” prop을 일일이 엮어주고 있습니다:

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 컴포넌트에서 별도의 "theme" prop을 받아서
  // ThemedButton 컴포넌트에 이를 넘겨주어야 합니다.
  // 만약 앱에서 사용되는 모든 버튼에 theme prop을 넘겨주어야 한다면
  // 이는 굉장히 힘든 작업이 될 것입니다.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

function ThemedButton(props) {
  return <Button theme={props.theme} />;
}

Context를 사용하면, 중간 계층에 위치하는 엘리먼트에 props를 넘겨주는 작업을 피할 수 있습니다:

// Context를 사용하면 prop을 일일이 엮어주지 않고도
// 컴포넌트 트리의 깊은 곳에 값을 넘겨줄 수 있습니다.
// 테마에 대한 context를 만들어줍시다. ("light"를 기본값으로 합니다.)
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Provider를 사용해서 현재 테마를 트리 아래쪽으로 넘겨줍시다.
    // 어떤 컴포넌트든 이 값을 읽을 수 있습니다. 아주 깊은 곳에 위치해있더라도 말이죠.
    // 아래에서는, "dark"라는 값을 넘겨주었습니다.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 이제 더이상 중간 계층에 있는 컴포넌트에서
// theme prop을 넘겨줄 필요가 없습니다.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  // 테마 context를 읽어오려면 Consumer를 사용하세요.
  // React는 가장 가까운 Provider를 찾아서 그 값을 사용할 것입니다.
  // 이 예제에서, theme 값은 "dark"가 됩니다.
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

주의

단지 몇 단계의 prop 전달을 건너뛰기 위해 context를 사용하지는 마세요. 여러 계층의 여러 컴포넌트에서 같은 데이터를 필요로 할 때에만 context를 사용하세요.

API

React.createContext

const {Provider, Consumer} = React.createContext(defaultValue);

{ Provider, Consumer } 쌍을 만듭니다. React가 context Consumer를 렌더링하면, 같은 context로부터 생성된 가장 가까운 상위 Provider에서 현재 context의 값을 읽어옵니다.

defaultValue 인수는 오직 상위에 같은 context로부터 생성된 Provider가 없을 경우에만 사용됩니다. 이 기능을 통해 Provider 없이도 컴포넌트를 손쉽게 테스트해볼 수 있습니다. 주의: Provider에서 undefined를 넘겨줘도 Consumer에서 defaultValue를 사용되지는 않습니다.

Provider

<Provider value={/* some value */}>

Context의 변화를 Consumer에게 통지하는 React 컴포넌트입니다.

value prop을 받아서 이 Provider의 자손인 Consumer에서 그 값을 전달합니다. 하나의 Provider는 여러 Consumer에 연결될 수 있습니다. 그리고 Provider를 중첩해서 트리의 상위에서 제공해준 값을 덮어쓸 수 있습니다.

Consumer

<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

Context의 변화를 수신하는 React 컴포넌트입니다.

function as a child 패턴을 사용합니다. 함수는 현재 context의 값을 받아서 React 노드를 반환해야 합니다. 트리 상위의 가장 가까이 있는 Provider의 value prop이 이 함수에 전달됩니다. 만약 트리 상위에 Provider가 없다면, createContext()에 넘겨진 defaultValue 값이 대신 전달됩니다.

주의

‘function as a child’ 패턴에 대한 자세히 알고싶으시면 render props 문서를 참고하세요.

Provider의 자손인 모든 Consumer는 Provider의 value prop이 바뀔 때마다 다시 렌더링됩니다. 이는 shouldComponentUpdate의 영향을 받지 않으므로, 조상 컴포넌트의 업데이트가 무시된 경우라 할지라도 Consumer는 업데이트될 수 있습니다.

Object.is 알고리즘을 통해 이전 값과 새 값을 비교함으로써 value prop이 바뀌었는지를 결정합니다.

주의

위 알고리즘 때문에, value prop에 객체를 넘기는 경우에 문제가 생길 수 있습니다: 주의사항을 확인하세요.

Examples

값이 변하는 Context

값이 변하는 theme value를 보여주는 좀 더 복잡한 예제입니다:

theme-context.js

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

export const ThemeContext = React.createContext(
  themes.dark // default value
);

themed-button.js

import {ThemeContext} from './theme-context';

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

export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// 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,
    };

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

  render() {
    // ThemeProvider 내부의 ThemedButton은
    // state에 저장되어 있는 theme을 사용하는 반면, 바깥에서는
    // 기본값으로 설정된 dark 테마가 사용됩니다.
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

ReactDOM.render(<App />, document.root);

중첩된 컴포넌트에서 context 갱신하기

컴포넌트 트리의 깊은 곳에 위치한 컴포넌트에서 context의 값을 갱신해야 하는 경우가 종종 있습니다. 이런 경우 함수를 아래로 넘겨주어 consumer가 context의 값을 갱신하게 만들 수 있습니다:

theme-context.js

// createContext에 넘겨주는 기본값의 모양이
// 실제 consumer에서 사용되는 값과 일치하도록 신경써주세요!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // ThemeTogglerButton 컴포넌트는 theme 뿐만 아니라
  // toggleTheme 함수도 받고 있습니다.
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

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

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

    // state가 갱신 함수도 포함하고 있기 때문에, 갱신함수 역시
    // provider로 넘겨질 것입니다.
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // 전체 state를 provider에 넘겨줍니다.
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

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

ReactDOM.render(<App />, document.root);

여러 context에서 값 넘겨받기

각 consumer를 별도의 노드로 만들어줄 수 있습니다.

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // App 컴포넌트에서 context 값을 제공하고 있습니다.
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

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

// 하나의 컴포넌트에서 여러 context의 값을 가져올 수 있습니다.
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

둘 이상의 context가 자주 함께 사용된다면, 이를 묶은 render prop 컴포넌트를 만드는 것을 고려해볼 수도 있습니다.

라이프사이클 메소드에서 context에 접근하기

라이프사이클 메소드에서 context 값을 사용해야 하는 경우가 있습니다. 이 때에는 값을 prop으로 넘겨준 뒤, 일반적인 prop을 다루듯이 다루면 됩니다.

class Button extends React.Component {
  componentDidMount() {
    // ThemeContext value is this.props.theme
  }

  componentDidUpdate(prevProps, prevState) {
    // Previous ThemeContext value is prevProps.theme
    // New ThemeContext value is this.props.theme
  }

  render() {
    const {theme, children} = this.props;
    return (
      <button className={theme ? 'dark' : 'light'}>
        {children}
      </button>
    );
  }
}

export default props => (
  <ThemeContext.Consumer>
    {theme => <Button {...props} theme={theme} />}
  </ThemeContext.Consumer>
);

Consuming Context with a HOC

Some types of contexts are consumed by many components (e.g. theme or localization). It can be tedious to explicitly wrap each dependency with a <Context.Consumer> element. A higher-order component can help with this.

For example, a button component might consume a theme context like so:

const ThemeContext = React.createContext('light');

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {theme => <button className={theme} {...props} />}
    </ThemeContext.Consumer>
  );
}

That’s alright for a few components, but what if we wanted to use the theme context in a lot of places?

We could create a higher-order component called withTheme:

const ThemeContext = React.createContext('light');

// This function takes a component...
export function withTheme(Component) {
  // ...and returns another component...
  return function ThemedComponent(props) {
    // ... and renders the wrapped component with the context theme!
    // Notice that we pass through any additional props as well
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
}

Now any component that depends on the theme context can easily subscribe to it using the withTheme function we’ve created:

function Button({theme, ...rest}) {
  return <button className={theme} {...rest} />;
}

const ThemedButton = withTheme(Button);

Forwarding Refs to Context Consumers

One issue with the render prop API is that refs don’t automatically get passed to wrapped elements. To get around this, use React.forwardRef:

fancy-button.js

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Use context to pass the current "theme" to FancyButton.
// Use forwardRef to pass refs to FancyButton as well.
export default React.forwardRef((props, ref) => (
  <ThemeContext.Consumer>
    {theme => (
      <FancyButton {...props} theme={theme} ref={ref} />
    )}
  </ThemeContext.Consumer>
));

app.js

import FancyButton from './fancy-button';

const ref = React.createRef();

// Our ref will point to the FancyButton component,
// And not the ThemeContext.Consumer that wraps it.
// This means we can call FancyButton methods like ref.current.focus()
<FancyButton ref={ref} onClick={handleClick}>
  Click me!
</FancyButton>;

주의사항

Context는 consumer를 다시 렌더링해야하는 시점을 결정하기 위해 값의 참조가 동일한지를 비교하기 때문에, provider의 부모가 렌더링될 때 consumer가 불필요하게 다시 렌더링되는 문제가 생길 수 있습니다. 예를 들어, 아래 코드는 Provider가 다시 렌더링될 때 모든 consumer를 다시 렌더링시키는데, 이는 value에 매번 새로운 객체가 넘겨지기 때문입니다:

class App extends React.Component {
  render() {
    return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
  }
}

이 문제를 회피하려면, value로 사용할 객체를 부모의 state에 저장하세요:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

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

Legacy API

Note

React previously shipped with an experimental context API. The old API will be supported in all 16.x releases, but applications using it should migrate to the new version. The legacy API will be removed in a future major React version. Read the legacy context docs here.