React - useContext

정지현·2024년 6월 10일

React

목록 보기
5/8
post-thumbnail

이전부터 계속 context를 한번 제대로 써보고 싶다는 마음은 있었지만 제대로 찾아보거나 하지는 않았었다. 하지만 사이드프로젝트를 진행하기에 앞서서, 스토어 라이브러리를 쓰려고 하자니 사이즈가 너무 작을 것 같고, 그렇다고 그냥 prop을 내리자니 너무 귀찮으니까 다시 한번 React.useContext를 찾게 되었는데 먼저 context의 가장 큰 문제인
'모든 children을 rerender하는' 것을 막는게 좋다고 생각했다.

하지만 먼저, context의 rerender 조건을 정리해 보자

1. Context는 왜 rerender를 유발할까?

TL;DR:
컨텍스트 내부의 밸류를 변경하는 것은, 해당 밸류에 의존하는 모든 자식 컴포넌트의 리렌더를 유발한다. 이를 막기 위해서는 밸류가 늘어날 때마다 프로바이더를 따로 만들어서 그 프로바이더로 감싸주어야만 한다.

먼저 가장 기본적인 리렌더 조건을 생각해 보자.
state가 변할 때, 해당 state와 관련된 모든 컴포넌트를 'recursive'하게 리렌더 한다. 먼저 부모 컴포넌트가 업데이트 뒨 뒤, 자식들을 돌면서 리렌더하게 된다.

그리고 context를 업데이트 하는 것도 위 부모 컴포넌트를 업데이트하는 것과 같다. 따라서 아무런 메모이징과 같은 테크닉이 없다면, 컨텍스트를 업데이트 하는 것은 결국 자식들을 모두 리렌더하는 것과 같다.

function GrandchildComponent() {
  const value = useContext(MyContext);
  return <div>{value.a}</div>;
}

function ChildComponent() {
  return <GrandchildComponent />;
}

function ParentComponent() {
  const [a, setA] = useState(0);
  const [b, setB] = useState('text');

  const contextValue = { a, b };

  return (
    <MyContext.Provider value={contextValue}>
      <ChildComponent />
    </MyContext.Provider>
  );
}

위의 예시 코드를 본다면, 리액트에서 권장하는 context 원칙과 다르다는 것을 알 수 있는데, 오브젝트로 데이터를 넘겼기 때문에, Provider안에 context를 사용하지 않는 컴포넌트를 만들더라도, 리렌더 시 오브젝트가 새롭게 만들어지기 때문에 렌더가 된다는 것을 알 수 있다. 이는 State로 만들어진 객체라도 마찬가지이다. value={state} 의 시점에서, 리액트는 이 밸류를 다른 객체가 할당되는것으로 인식하기 때문이라고 한다. 이를 막기 위해서는,

return (
    <MyContext.Provider value={a}>
      <ChildComponent />
      <AnotherContext.Provider value={b}>
        <ChildComponentConsumesB />
      </AnotherContext.Provider>
    </MyContext.Provider>
  );

와 같이 밸류를 하나의 데이터로 하고 프로바이더를 쪼개서 렌더하는것이 바람직하다고 한다.

2. 하지만 저건 좀 너무 불편..

불편한 문제보다는, 컴포넌트를 나누는데에 너무 힘들 것 같다.. 조금 더 좋은 대안을 찾아보자.

위의 레퍼런스에서는 자식 컴포넌트를 memo 하는 것으로

위의 코드에서 이어져서,

function GreatGrandchildComponent() {
  return <div>Hi</div>
}

function GrandchildComponent() {
    const value = useContext(MyContext);
    return (
      <div>
        {value.a}
        <GreatGrandchildComponent />
      </div>
}

function ChildComponent() {
    return <GrandchildComponent />
}

const MemoizedChildComponent = React.memo(ChildComponent);

function ParentComponent() {
    const [a, setA] = useState(0);
    const [b, setB] = useState("text");

    const contextValue = {a, b};

    return (
      <MyContext.Provider value={contextValue}>
        <MemoizedChildComponent />
      </MyContext.Provider>
    )
}

1의 예시와 다르게, 밸류를 받는 첫 차일드가 메모이즈되었다.

이 경우, setA를 통해 스테이트가 업데이트 되었을 때,

a. ParentComponent가 리렌더된다.
b. const contextValue = {a, b}; 가 재정의된다.
c. Provider에 새 오브젝트가 할당되어 consumer 쪽의 리렌더가 시작된다.
d. 먼저 ChildComponent 리렌더되어야 하지만, memo를 통해 prop의(사실 props 자체가 없으므로) 변경을 감지하지 못했으므로 childComponent는 리렌더 되지 않는다.
e. 리렌더 페이즈가 끝난 것이 아니므로, 계속해서 다른 컴포넌트로 이동한다.
f. GrandchildComponent는 useContext를 사용하고 있으므로 리렌더된다.
g. GreatGrandchildComponent 는 자신의 부모가 리렌더 되었으므로, 리렌더된다.

이것을 통해서, context의 자식이라도 해당 업데이트 된 밸류를 사용하지 않는 컴포넌트라면 memo를 통해 prop을 체크해서 리렌더를 막을 수 있다는 것을 알게되었다. 하지만, GrandchildComponent의 예시에서 알 수 있듯이, 컨텍스트 밸류를 사용하는 컴포넌트의 자식 컴포넌트는 리렌더 되는 것이 기본 규칙이므로, 이 컨텍스트 밸류를 참조하지 않는 모든 컴포넌트에 대해 memo를 사용해야만 한다! 물론, 컨텍스트에 하나 이상의 밸류가 들어간다면, 사용하는 컴포넌트도 메모를 사용해야 할 것이다.

다음 글에서는 더 좋은 대안이 있을까? 에 대해서 찾아보게 될 것 같다.
사실 이미 찾았지만..
Jack Harrington 선생님의 Fast Context

참고한 url:

https://www.youtube.com/watch?v=ZKlXqrcBx88
https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/#context-and-rendering-behavior

profile
Can an old dog learn new tricks?

0개의 댓글