React Context

이 글을 통해 React의 Context 개념과 사용에 대해 간단히 알아보고, 실제로 context를 사용하여 간단한 컴포넌트를 제작해보겠습니다.

1. Context란?

처음 React로 개발을 진행할 때, 컴포넌트 간에 데이터를 전달하는 데 있어 불편함을 겪었습니다. 부모 컴포넌트가 자식 컴포넌트에게 데이터를 '직접' 전달해야만 했기 때문입니다. 데이터를 여러 컴포넌트들이 사용하는 경우에는 각각의 컴포넌트의 props로 넘겨줘야 하는 불편함이 있고, 컴포넌트 구조에 따라 props로 넘겨줄 수 없는 상황도 있었습니다.

데이터 흐름

이러한 불편함을 해소해줄 수 있는 것이 바로 Context입니다. Context는 데이터를 전역적으로 사용할 수 있는 방법입니다. Context를 사용하면, 값을 직접 전달하는 것이 아니라, 부모 컴포넌트에서 값을 저장 및 관리하고, 값을 사용하는 자식 컴포넌트에서 Context 의 값을 사용할 수 있습니다.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Context의 동작을 간단히 요약하면, 1. context를 생성한 후, 2. Provider를 통해 context의 값을 설정하고, 3. 해당 context를 구독하는 컴포넌트가 context로부터 데이터를 꺼내 사용하는 것입니다.

생성된 context는 자신을 구독하고 있는 컴포넌트가 render될 때 트리 내 부모 중에 가장 가깝고 적합한 Provider를 찾습니다. 해당 Provider가 제공하는 값(value)를 받아서 구독하고 있는 컴포넌트에게 전달해줌으로써 context를 통한 데이터 전달이 완료됩니다. 이러한 데이터 전달은 일회성에 그치는 것이 아니라 Provider가 context의 값을 바꾸면 데이터를 받아서 context를 구독하고 있는 컴포넌트들이 rerender됩니다.

2. Context 사용

위에서 언급한 세 가지 단계를 코드로 살펴보겠습니다.

1) Context 생성 - React.createContext

React.createContext 를 통해 Context 를 생성합니다.

const FirstContext = React.createContext(defaultValue)

Context 를 생성할 때 defaultValue 를 통해 생성하며 이 때 defaultValue 는 이후 설명할 Context.Provider 에서 설정되지 않은 값의 기본 값을 설정합니다.

2) value 설정 - Context.Provider

Context.Provider 는 자식 컴포넌트에게 값을 전달합니다.

<FirstContext.Provider value = {
    hello:"Hello",
    hi:"Hi"
  }
/>

value 의 값이 Context.Consumer 하는 자식 컴포넌트에게 전달됩니다. 중첩된 Context.Provider 구조를 가질 수 있으며, 중첩된 구조의 경우 가장 근접한 Providervalue를 가져오게 됩니다.
위에서 언급하였듯이, Providervalue가 변화하면 context를 구독하는 하위 컴포넌트들은 rerender하게 됩니다.

3) 구독 - Context.Consumer / contextType

Context를 구독하는 방법에는 두 가지가 있습니다. 첫번째는 Consumer를 사용하는 방법입니다.

<FirstContext.Cosumer>
  {(hello, hi)=>(
    <div>
      {hello}{hi}
    </div>
  )}
</FirstContext.Cosumer>

두번째는 contextType을 사용하는 방법입니다.

class HiHello extends React.Component {
  render() {
    const value = this.context;
    
    return (
        <div>
          {hello}{hi}
        </div>
    );
  }
}
HiHello.contextType = FirstContext;

또는

class HiHello extends React.Component {
  static contextType = FirstContext;
  context!: React.ContextType<typeof FirstContext>; // 타입 추론이 가능해집니다.
  
  render() {
    const value = this.context;
    
    return (
        <div>
          {hello}{hi}
        </div>
    );
  }
}

contextType으로 설정한 Context.Provider의 값은 this.context을 이용하여 값을 가져올 수 있습니다.
contextType를 통해서는 가장 가까운 하나의 Context.Provider의 값만 사용 할 수 있으며 여러 Context.Provider의 값을 사용해야하는 경우 Context.Consumer를 사용해야 합니다.

3. 예시

프론트 개발을 하면서 context를 사용하는 경우는 주로 컴포넌트가 서브 컴포넌트를 가지는 경우입니다. 컴포넌트를 설계시, 사용성을 위해 하나의 컴포넌트가 여러 서브 컴포넌트로 가지는 구조로 설계하기도 합니다. 이 때 컴포넌트와 서브 컴포넌트는 데이터를 주고 받아야하는 경우가 잦은데, 직접 props로 넘겨줄 수가 없습니다. 이에 대한 예제로 정말 간단한 Modal 컴포넌트를 제작해보겠습니다. 흔히 접하는 alert와 confirm창이 modal의 일종입니다. 텍스트와 하단의 버튼으로 구성되어 있으며, 일반적으로 트리거가 되는 버튼을 누르면 열리고 닫기 버튼을 누르면 닫힙니다. Modal 컴포넌트에 context가 필요한 이유는 modal의 open에 대한 상태가 컴포넌트 내부에서 공유되어야 하기 때문입니다.

Material ui의 Dialog를 사용하여 제작해보겠습니다. (Material ui의 Dialog api는 https://material-ui.com/components/dialogs/#dialog를 참고하세요.)

간단한 modal의 동작
modal 컴포넌트 구조

Modal 컴포넌트의 구조는 위와 같이 설계하였습니다. 이렇게 설계시, modal 컴포넌트 사용은 아래와 같습니다.

<SampleModal>
  {/* modal 내용 */}
  <SampleModal.Content>
  	sample modal!
  </SampleModal.Content>

  {/* modal 닫기 버튼 */}
  <SampleModal.Actions />
</SampleModal>

1) context 생성

modal의 open을 false로 바꿔줄 close 메소드 갖는 context를 만듭니다.

// context/ModalContext.ts
import React from 'react';

export type ModalContextModel = {
  close: () => void;
}

const ModalContext = React.createContext<ModalContextModel>({
  close: () => {},
});

export default ModalContext;

2) Provider로 context의 값 설정

Modal 컴포넌트의 최상위에 위치할 ModalContainer가 Provider를 제공합니다.

// ModalContainer.tsx
interface State {
  open: boolean;
}

class ModalContainer extends React.Component<Props, State> {
  //
  state: State = {
    open: false,
  };

  getContext(): ModalContextModel {
    //
    return {
      close: this.close,
    };
  }

  open() {
    //
    this.setState({ open: true });
  }

  close() {
    //
    this.setState({ open: false });
  }

  render() {
    //
    const { children } = this.props;
    const { open } = this.state;

    return (
      <ModalContext.Provider value={this.getContext()}>
        <Button variant="contained" color="primary" onClick={this.open}>
          trigger
        </Button>
        <Dialog open={open}>{children}</Dialog>
      </ModalContext.Provider>
    );
  }
}

export default ModalContainer;

3) context 사용

context를 구독하는 두 가지 방법 중 contextType을 사용하여 열린 modal을 닫을 수 있는 Actions 서브 컴포넌트를 작성하겠습니다.

// sub-comp/ModalActions/ModalActionsView.tsx
class ModalActionsView extends React.Component {
  //
  static contextType = ModalContext;
  context!: React.ContextType<typeof ModalContext>;

  onClick(event: React.MouseEvent<HTMLButtonElement>) {
    //
    this.context.close();
  }

  render() {
    //
    return (
      <DialogActions>
        <Button onClick={this.onClick}>
          닫기
        </Button>
      </DialogActions>
    );
  }
}

export default ModalActionsView;

전체 코드는 아래를 참고하세요.

이상 Context에 대해 알아보았습니다. 데이터 관리는 React의 Context뿐만 아니라 Redux와 Mobx와 같은 다른 라이브러리를 통해서도 가능합니다. Mobx에 대한 구체적인 내용은 아래의 영상에서 확인하실 수 있습니다.

[ React + MobX ] What is React?
React에 대한 기본적인 개념과 구조에 대해서 설명합니다.#React #MobX #리액트 #SPA

감사합니다.