[React] Context

Context

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


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


언제 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>
  );
}


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 값이 대신 전달됩니다.

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

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


Example - 값이 변하는 Context

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= />
      )}
    </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);


Example - 중첩된 컴포넌트에서 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=
        >
          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);


Example - 여러 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>
);


Context 주의사항

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

class App extends React.Component {
  render() {
    return (
      <Provider value=>
        <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>
    );
  }
}


[ Context : 꼭 알아야 하는 두가지 교훈]

  1. 최상위 컴포넌트에서 최하위 컴포넌트로 손쉽게 데이터나 함수 를 내릴 수 있다.
  2. 외부세계의 역할만을 책임지는 콤포넌트를 만듦으로써 유지보수성을 높일 수 있다.