React Context
포스트
취소

React Context

Context

Context는 React에서 제공하는 기능 중 하나로, 유효범위 내부에 존재하는 컴포넌트들에서 데이터를 주고받는 방법 중 하나입니다.

사용 이유

기본적으로 React 공식 홈페이지에서는 컴포넌트들의 계층이 복잡해질 때 props가 아닌 다른 방식으로 데이터를 쉽게 주고 받을 수 있다고 홍보(?)하고 있습니다. Prop Drilling 하위 컴포넌트에게 넘겨주기 위해 거쳐가야하는 관문이 많다

위 사진은 저번 Atomic Design 문서에서 언급했던 Prop Drilling 현상을 설명하는 사진입니다. 컴포넌트가 서로 데이터를 공유하기 위해 props를 사용하다보면 상위 계층이 갖고있는 데이터를 최하위까지 넘겨주기 위해 생기는 문제가 발생하는데 이를 Prop Drilling이라고 합니다.

Context는 이를 어떻게 해결해 줄 수 있을까요?

사용법

React.createContext

우선 Context를 사용하기 위해 Context를 생성해봅시다.
Context를 생성하기 위해서는 React.createContext 함수를 사용해주시면 됩니다. 매개변수로는 해당 Context가 생성 초기에 가질 데이터를 선언해주면 됩니다.

1
2
3
const ExampleContext = React.createContext({ message: 'Hello world!' });

export default ExampleContext;

Context를 선언했으니 이제 이를 사용해봐야겠죠?

Context.Consumer

Consumer라는 이름을 가진 API답게 Context의 값을 사용만 할 수 있는 컴포넌트입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ExampleContext from './context/ExampleContext.ts';

export const App = () => {
  return (
    <ExampleContext.Consumer>
      {(value) => {
        return (
          <div>
            오늘의 인사말은 {value.message} 입니다!
          </div>
        );
      }}
    </ExampleContext.Consumer>
  );
};

export default App;

Context API에서 자주 사용되는 ConsumerContext가 갖고있는 value에 접근할 수 있는 기능을 제공합니다.
Context.Consumer 태그 내부에는 함수가 적용될 수 있는데, value를 매개변수로 갖고 있으며 해당 함수에서 반환되는 JSX를 렌더링합니다.

value는 일반적인 State와 마찬가지로 반응성을 가지고 있어, Context의 데이터가 변경이 된다면 Context의 값을 참조하고 있는 컴포넌트들은 모두 리렌더링이 됩니다.

Context.Provider

ProviderConsumer와 동일하게 값을 참조할 수 있지만 데이터를 이 곳에서 선언할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import ExampleContext from './context/ExampleContext.ts';

export const App = () => {
  const [message, setMessage] = useState<string>('Good bye!');

  return (
    <ExampleContext.Provider value={{ message }}>
      <div>오늘의 인사말은 {message} 입니다!</div>
    </ExampleContext.Provider>
  );
};

export default App;

React.useContext

useContextProvider 내부에 존재하는 계층구조의 컴포넌트에서 최하위 계층에서 Context값을 참조하는데 도움을 주는 API입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import ExampleContext from './context/ExampleContext.ts';
import MessagePrintComponent from './MessagePrintComponent.tsx';

export const App = () => {
  return (
    <ExampleContext.Provider value={{ message: 'good bye!' }}>
      <MessagePrintComponent />
    </ExampleContext.Provider>
  );
};

export default App;

위와 같이 Provider 내부에 존재하는 MessagePrintComponent가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import ExampleContext from './context/ExampleContext.ts';

export const MessagePrintComponent = () => {
  const value = React.useContext(ExampleContext);

  return (
    <div>
      <div>오늘의 인사말은 {value.message} 입니다!</div>
    </div>
  );
};

export default MessagePrintComponent;

useContext를 사용하면 Provider 하위에 존재하는 컴포넌트에서도 굳이 props로 받지 않더라도 데이터를 직접 참조할 수 있습니다.

그게 ‘가능’은 하다는거지 컴포넌트의 의존성이 심해지므로 일반적인 경우에는 props로 데이터를 전달받는게 훨씬 좋습니다.

주의사항

리렌더링 이슈

React에서의 컴포넌트 리렌더링은 어떤 컴포넌트에서 사용되고 있는 state의 참조를 확인합니다.
그렇다보니 만약 Context가 최상위가 아니라 페이지 전체 계층 중 중간쯤 단계에서 사용하고 있을 때 이런 문제가 발생할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
const Top = () => {
  const [number, setNumber] = useState<number>(0);

  return (
    <>
      <Middle number={number} />
      <button onClick={setNumber(number + 1)}>number 증가</button>
    </>
  );
};

1
2
3
4
5
6
7
8
9
10
export const Middle = (props) => {
  return (
    <AnyContext.Provider value={{ message: 'Hello world!' }}>
      { props.number }
      <Bottom />
    </AnyContext.Provider>
  );
};

1
2
3
4
5
6
7
8
9
10
11
export const Bottom = () => {
  const value = React.useContext(AnyContext);

  return (
    <div>
      { value.message }
    </div>
  );
};

  1. Top 컴포넌트는 number라는 state를 갖고 있습니다.
  2. Top 컴포넌트는 버튼을 누르면 number가 1씩 증가합니다.
  3. Top 컴포넌트는 Middle 컴포넌트에게 number를 전달합니다.
  4. Top 컴포넌트는 Middle이라는 하위 컴포넌트를 갖고 있습니다.
  5. Middle 컴포넌트는 AnyContext.Provider를 갖고 있습니다.
  6. Middle 컴포넌트는 Bottom이라는 하위 컴포넌트를 갖고 있습니다.
  7. Middle 컴포넌트는 상위 컴포넌트로부터 number를 받아서 출력합니다.
  8. Bottom 컴포넌트는 useProvider라는 API를 이용하여 AnyContext의 데이터를 참조하고 있습니다.

이 상황에서 Bottom 컴포넌트가 렌더링 되기 위한 최고의 상황은 AnyContextValue가 수정될 때 뿐일거예요.
하지만 setNumber가 실행될 때마다 Bottom마저 리렌더링이 됩니다.

그 이유는, Top 컴포넌트의 statemiddle 컴포넌트가 바라보고 있으니 number가 변할 때마다 middle이 리렌더링 됩니다.
그 과정에서 Provider가 리렌더링 되면서 전달되는 value의 값마저 재정의 되기 때문에 React는 해당 Provider하위에서 AnyContext의 value를 참조하는 모든 컴포넌트를 리렌더링 시키게 됩니다.

해결방법

Provider에 전달할 value를 state로 정의하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
export const Middle = (props) => {
  const [message, setMessage] = useState<string>('Hello world');

  return (
    <AnyContext.Provider value={{ message }}>
      { props.number }
      <Bottom />
    </AnyContext.Provider>
  );
};

이렇게 정의하면 상위 컴포넌트의 값이 바뀌어서 Middle 컴포넌트가 리렌더링이 된다고 해도 상태로서 정의 된 message의 값이 재정의 되지 않으니 React는 Provider의 value가 변경되었다고 판단하지 않기 때문입니다.

레퍼런스

React - Context

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.