왜 React는 리렌더링을 하는가

기운찬곰·2023년 5월 5일
0

React Core Concepts

목록 보기
1/7
post-thumbnail

참고(원본 글) : https://www.joshwcomeau.com/react/why-react-re-renders/
참고 2 : https://www.seonest.net/posts/react/why-react-re-renders

실습 : https://ckstn0777.github.io/react-playground/

📚 joshwcomeau 블로그에 있는 'Why React Re-Renders' 라는 글이 좋아서 번역하고 실습해서 정리한 글입니다. 제가 강력 추천하는 글이니 한번쯤 읽어보면 좋을 듯 합니다.

Overview

리액트 개발자에게 "리액트에서 리렌더링을 일으키는 것은 무엇인가요?" 라고 물어보면 다양한 답변을 얻을 수 있을 것이다. 이 주제에 대해 많은 오해들이 있고, 그것은 많은 불확실성으로 이어질 수 있다.

리액트의 렌더링 사이클을 이해하지 못한다면 React.memo를 사용하는 방법이나 언제 useCallback을 함수에 감싸서 사용해야 하는지 알기 쉽지 않고 이해하기도 어려울 것입니다.

이 글에서는 React가 리렌더링되는 시기와 이유에 대해 알아볼 것입니다. 또한 React devTools를 사용하여 특정 컴포넌트가 리렌더링되는 이유를 설명하는 방법에 대해서도 배워봅니다.


The Core React loop

리액트에서 모든 리렌더링은 상태(state) 변경에서 시작합니다. 과거 forceUpdate() 메서드가 없어진 이후, 컴포넌트가 리렌더링 되는 유일한 트리거인 셈입니다.

근데 이상합니다. props가 변경될 때도 컴포넌트는 리렌더링 되니까요. context를 사용해도 마찬가지 입니다. 이런 의문에 대한 답은 "컴포넌트가 리렌더링 될 때 단지 모든 하위 컴포넌트 또한 리렌더링되기 때문" 이라고 설명할 수 있습니다. 🤔

이게 무슨 말일까요? 일단 아래 예시를 보겠습니다.

function App() {
  return <Counter />;
}

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <main>
      <BigCountNumber count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </main>
  );
}

function BigCountNumber({ count }) {
  return (
    <p>
      <span className="prefix">Count:</span>
      {count}
    </p>
  );
}

export default App;
  • Counter에 버튼을 클릭할 때 마다 Counter과 그 하위 컴포넌트인 BigCountNumber 가 리렌더링 될 것입니다.

  • App 컴포넌트는 count 상태가 변경되도라도 리렌더링 되지 않습니다. 이것을 통해 알 수 있는 점은 리렌더링은 상태 + 하위 컴포넌트(존재한다면)에만 영향을 미친다는 것입니다.

Chrome 개발자 도구에서 Profiler를 사용하면 무엇이 리렌더링 되었고, 리렌더링이 얼마나 걸렸는지, 왜 리렌더링이 발생했는지 등을 자세히 알 수 있습니다.

Counter 컴포넌트에 대해서는 Hook 1 changed 때문에 리렌더링되었다고 하네요. count의 상태가 바뀌었기 때문이겠죠.

Counter 하위 BigCountNumber 컴포넌트는 Props changed (count) 때문에 리렌더링된 것을 알 수 있네요.

그렇다면... 리액트는 왜 하위 컴포넌트까지 리렌더링 시키는 걸까요? 🤔

리액트는 상태와 어플리케이션 UI를 동기화 시키기 위해 리렌더링을 합니다. 그리고 리렌더링의 중점은 변경이 필요한 부분을 파악해서 변경하는 것입니다. 그러기 위해서는 기존 UI를 스냅샷을 찍은 다음, 새로 변경된 부분을 기존에서 틀린그림찾기(diff 알고리즘) 같은 비교를 통해 수정이 필요한 부분을 판단해서 그 부분만 교체해줍니다.

하위 컴포넌트까지 리렌더링되는 것은 잠재적으로 영향을 받을 수 있는 부분이기 때문입니다. 상태가 변경되면 React는 하위 컴포넌트가 영향을 받는지 아닌지 정확히 알 수 없어서 기본적으로 리렌더링 시키는 것입니다.

와... 저 설명 속에 리액트의 핵심 개념과 철학이 들어가 있어서 무슨 의미인지 바로 알 수 있었습니다. 리스펙...


It’s not about the props

"props가 변경되어서 컴포넌트가 리렌더링 되는 것이다". 사실 정확히 말해서는 이 말은 틀린 거라고 합니다.

아래 예시를 보시죠. Decoration 만 Counter에 하위 컴포넌트로 추가했습니다.

function App() {
  return <Counter />;
}

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <main>
      <BigCountNumber count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>

      <Decoration />
    </main>
  );
}

function BigCountNumber({ count }) {
  return (
    <p>
      <span className="prefix">Count:</span>
      {count}
    </p>
  );
}

function Decoration() {
  return <div className="decoration">⛵️</div>;
}

export default App;
  • Decoration 컴포넌트는 props로 count를 받지 않기 때문에 의존하지 않는다. 그러나 BigCountNumber와 마찬가지로 리렌더링되는 것을 알 수 있다.
  • 결국, 정리해보면 한 컴포넌트가 리렌더링되면 props를 통해 특정 상태가 전달되는지 여부와 상관없이 모든 하위 컴포넌트가 리렌더링된다는 것이다.

아... "props가 변경되어서 컴포넌트가 리렌더링 되는 것이다" 라는 말도 맞긴 하지만, 더 큰 범위에서는 "props와 관련없이 모든 하위 컴포넌트가 리렌더링 되는 것이다" 라는 말이 더 맞겠네요.

Chrome Profiler에서 리렌더링 되는 이유에 대해 “The parent componet rendered”라고 보여주고 있네요.

그렇다면 왜 Decoration을 리렌더링 하는 것일까?

위에서도 설명했듯이 리액트는 Decoration 컴포넌트가 count 상태에 직간접적으로 의존하는지 100% 알기 어렵기 때문입니다.

이상적인 경우 리액트 컴포넌트는 항상 pure(순수)해야 합니다. 순수하다는 것은 동일한 props가 주어지면 항상 같은 UI를 만들 수 있다는 것을 의미합니다. 하지만 현실세계에서 많은 컴포넌트들이 순수하지 않습니다. 그렇기 때문에 리액트에서 기본적으로 모든 하위 컴포넌트까지 리렌더링 시키는 것입니다.

리액트의 중요한 목표는 상태와 어플리케이션 UI를 동기화하는데 있습니다. 사용자에게 오래된 UI를 보여주는 위험을 감수하고 싶지 않습니다.

오호. 결국 그런 위험성을 감수하고 싶지 않아서 기본적으로 모든 하위 컴포넌트까지 리렌더링 시킨다는 거네요.

이것이 리액트의 기본적인 운영 방식이지만, 조금 조정할 수 있는 방법이 있습니다.


Creating pure components

Decoration 컴포넌트가 리렌더링되는 것을 막는 방법이 없는 것은 아닙니다. 아마도 리액트 개발자라면 React.memoReact.PureComponent에 대해 알고 있을 것입니다. 이 2가지 도구는 특정 리렌더링 요청을 무시할 수 있게 해줍니다.

예시를 보겠습니다.

function Decoration() {
  return <div className="decoration">⛵️</div>;
}
export default React.memo(Decoration);

Decoration 컴포넌트를 React.memo로 감싸서, 리액트에게 “이 컴포넌트는 순수하니까 props가 변경되지 않는 한 리렌더링 하지 않도록“ 말할 수 있다. 실제로 적용해보면 Profiler에 더 이상 Decoration 컴포넌트가 보이지 않는다.

이것이 우리가 메모리제이션(memoization) 으로 알고 있는 기술입니다. 이 아이디어는 리액트가 이전 스냅샷을 기억한다는 것에서 출발합니다. 변경되는 props가 없다면 리액트는 새롭게 만들지 않고 이전의 스냅샷을 재사용합니다. 따라서 BigCountNumber 컴포넌트를 React.memo로 감싸봤자 이전과 동일한 결과가 나옵니다. 왜냐면 props가 계속 변경되니까요.

그런데 궁금한 점이 있습니다. 리액트에서 이것을 왜 기본동작으로 하지 않을까요? 그니까 왜 굳이 React.memo로 번거롭게 감싸줘야되는지... 🤔

이에 대해서는 개발자로써 리렌더링에 드는 비용을 과대평가하는 경향이 있기 때문에 그렇게 말하는 것입니다. Decoration 컴포넌트 경우 리렌더링은 빛의 속도만큼 빠릅니다. 만약 어떤 컴포넌트가 거대한 props를 가지고 하위 컴포넌트가 많지 않은 경우라면, 리렌더링하는 것과 비교하여 props가 변경되었는지를 확인하는 것이 실제로 더 느리게 동작할 수 있습니다.

아하. 기존은 그냥 리렌더링하면 되지만 React.memo를 감싸면 props가 변경되었는지 매번 확인을 하니까 느릴 수 있겠네요. 따라서 기본 동작은 React.memo를 감싸지 않은 형태로 동작하는게 맞네요.


Bonus: Performance tips

리액트에서 성능 최적화는 거대한 주제입니다. 리액트 성능 최적화에 대한 몇 가지 팁을 공유드립니다:

  • 리액트 프로파일러에서 보여주는 렌더링 시간은 실제 동작시간과 다릅니다. 우리는 일반적으로 "development 모드"에서 프로파일합니다. 리액트는 "production 모드"에서는 훨씬 더 빠릅니다. 따라서 어플리케이션 성능을 실제로 이해하기 위해서는 배포된 production 어플리케이션에서 "Performance" 탭을 사용해 측정해야 합니다.
  • 90 백분위수에서의 사용자 경험이 어떤지 확인하기 위해 애플리케이션을 저성능 하드웨어에서 테스트하는 것이 좋습니다. 나(저자)는 몇 년 전 인도에서 인기 있었던 저가 스마트폰인 샤오미 레드미 8에서 정기적으로 물건들을 테스트합니다.
  • Lighthouse 성능 점수는 실제 사용자 경험을 정확하게 반영하지 않습니다. 나(저자)는 어떤 자동화된 도구가 보여주는 통계보다 애플리케이션을 사용하는 질적인 경험을 훨씬 더 신뢰합니다.
  • 무리하게 최적화하지 마세요! React 프로파일러에 대해 배울 때 렌더링 수를 최대한 줄이는 것을 목표로 최적화 작업을 계속하는 것은 매력적이지만, React는 이미 최적화가 잘 되어있습니다. 이러한 도구(리액트 Profiler, Lighthouse, ...)는 성능 문제에 대응하기 위해 사용하는 것이 가장 좋습니다. 눈에 띄게 성능이 안 좋아졌다면 말이죠.

마치면서

해당 글을 처음 읽었을 때 와... 리액트의 핵심 개념과 철학을 바로 이해할 수 있어서 좀 놀랐습니다. 정말 좋은 글이니 여러분들도 안 보셨다면 꼭 한번쯤 보셨으면 좋겠습니다. (추천!!)

그리고 저는 React.memo에 대해서는 사실 잘 안 쓰기도 하고, 계속 안 쓰다보니 굳이 써야 하나? 라는 생각이 들었는데... 왜 필요하고 언제 사용하면 좋은지 이제야 제대로 이해가 되는거 같습니다.

하지만 위에서도 나와있듯이 무리한 최적화는 좋은 방향은 아니며, 애플리케이션이 눈에 띄게 느려졌다면 원인을 분석해 최적화를 하는 게 좋을 거 같습니다.

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글