리렌더링 최적화를 하기 위해서는 먼저 리렌더링이 발생하는 조건을 알아야 한다.

1️⃣ 리렌더링 발생 조건

1. state가 정의된 컴포넌트에서 state가 변경되었을 때

  • 값이 변화했을 때 리렌더링을 발생시키기 위해 state를 사용하는 것이기 때문에, 이는 너무나 당연한 이야기이다.
  • 만약 리렌더링을 의도하지 않았다면, useRef를 사용하면 된다.

2. 컴포넌트에서 받는 props의 값이 변경되었을 때

  • props로 받는 값을 항상 최신 상태로 유지해야 하기 때문에, 이 또한 너무나도 당연한 이야기이다.

3. 컴포넌트에서 받는 props의 부모 컴포넌트가 렌더링되었을 때

  • 이 상황에서도 당연하게 리렌더링이 발생한다고 할 수 있을까?

2️⃣ 메모이제이션

  • 계산 결과를 메모리에 저장해두고 동일한 계산이 요청될 때, 계산을 다시 요청하지 않고 저장된 값을 반환하는 최적화 기법이다.

  • 메모이제이션을 하는 대상에 따라 크게 3가지 방법으로 나눌 수 있다.

    • React.memo: 컴포넌트를 메모이제이션
    • useMemo: 연산의 결과 값을 메모이제이션
    • useCallback: 연산을 수행하는 함수를 메모이제이션

✏️ React.memo

  • 컴포넌트를 메모이제이션하는 고차 함수

  • React에서 제공하는 고차 컴포넌트(HOC)로, 컴포넌트 렌더링 결과를 메모이징하여 성능을 최적화한다.

  • 해당 컴포넌트가 받는 props가 동일한지 얕은 비교를 통해 검사를 진행한다.

    • props 값이 이전 props 값과 동일하다면 > 부모 컴포넌트가 리렌더링 되어도 자식 컴포넌트는 리렌더링되지 않는다.
    • props 값이 이전 props 값과 다르다면 > 부모 컴포넌트가 리렌더링 되면, 자식 컴포넌트도 리렌더링 된다. (이 때, 부모 컴포넌트에서 관리하는 state 값이 변경되어도 리렌더링이 발생한다.)
      const SomeComponent = React.memo(function
         SomeComponent(props) {
            // …
         });

  • 두 번째 인자로 커스텀 비교 함수를 받아 깊은 비교를 통해 검사를 진행할 수 있다.
    • 이전 props(prevProps)와 새로운 props(nextProps)를 인자로 받는다.
    • 비교 함수가 true를 반환할 경우 > 리렌더링이 발생한다.
    • 비교 함수가 false를 반환할 경우 > 리렌더링이 발생하지 않는다.
      function areEqual(prevProps, nextProps) {
        /*
         nextProp가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
        */
      }
      export default React.memo(MyComponent, areEqual);

🚀 주의 사항 (참조 동일성 유지의 필요성)

  • 컴포넌트의 props가 이전 props 값과 동일한지 얕은 비교를 통해 검사하기 때문에, 객체나 배열, 함수 같은 참조 타입의 props를 검사할 때 문제가 발생할 수 있다.
  • 예를 들어, 객체와 함수를 props로 받는 경우 객체와 함수는 참조 타입이기 때문에, 매 리렌더링마다 내용은 같지만 참조가 달라져 props가 변경되었다고 판단 > 리렌더링이 발생한다.

✏️ useMemo

  • 연산의 결과 값을 메모이제이션하여 성능을 최적화하기 위한 훅

  • useMemo 사용 시, 값의 참조 동일성을 유지할 수 있다.

  • 첫 번째 인자로 메모이제이션할 연산, 두 번째 인자로 의존성 배열을 받는다.
    의존성 배열에 포함된 값이 변경되지 않으면 이전 결과 값을 재사용한다. (useEffect와 유사한 구조)

    function School (props) {
       const name = useMemo((){
          return {
             lastname: ‘서’,
             firstname: ‘아름’
          };
    }. []);
  • useMemo를 활용하여 값을 할당하면 의존성 배열에 포함된 값이 변경될 경우에만 재할당이 일어나기 때문에, 값의 참조 동일성을 보장한다.

  • 의존성 배열이 비어있는 경우, 초기 렌더링 시에만 첫 번째 인자에 해당하는 연산을 수행한다.


✏️ useCallback

  • 함수를 메모이제이션하여 성능을 최적화하기 위한 훅

  • useCallbck 사용 시, 함수의 참조 동일성을 유지할 수 있다.

  • 첫 번째 인자로 메모이제이션할 연산, 두 번째 인자로 의존성 배열을 받는다.
    의존성 배열에 포함된 값이 변경되지 않으면 이전 함수를 재사용한다. (useMemo와 동일한 구조)


🚀 흔한 오해

  • useCallback으로 메모이제이션한 함수는 의존성 배열에 포함된 값이 변하지 않을 경우, 리렌더링 시 새롭게 생성되지 않는다고 생각하기 쉽다.

  • 정말 그럴까?? ❌❌

  • useCallback의 인자로 넘겨주는 함수는 매 렌더링마다 반드시 다시 생성된다.

  • 이후, 의존성 배열을 검사하여 이전 함수와 새롭게 생성된 함수가 다르다면 생성한 함수를 리턴해주고,
    변하지 않았다면 기존에 메모이징했던 함수를 재사용한다 !!


❓ useMemo, useCallback은 React.memo 없이는 렌더링 최적화에 아무런 효과가 없을까 ?

React.memo로 컴포넌트 리렌더링을 최적화한 뒤, 내부적으로 useCallback과 useMemo를 활용하여 연산된 값이나 함수를 메모이징할 수 있다.
여기서 React.memo만 제거해보자.

기존에 리렌더링 최적화를 해둔 부분에서 다시 리렌더링이 발생하는 것을 확인할 수 있다.
그렇다면 useMemo, useCallback은 React.memo 없이는 렌더링 최적화에 아무런 효과가 없는걸까?

  • React.memo는 컴포넌트 메모이제이션을 수행한다.
    이러한 역할이 사라지니 더이상 컴포넌트가 메모이제이션 되지 않아 기존에 최적화해두었던 부분에서 모두 다시 리렌더링이 발생하는 것이다.

  • 해당 질문에 대한 답은 React의 리렌더링 과정을 이해하면 알 수 있다.


✏️ React의 리렌더링 과정

▶️ Render Phase

  • 재조정 과정을 거쳐 Virtual DOM과 Real DOM을 비교, 변경이 필요한 부분을 확인한다.

▶️ Commit Phase

  • Render Phase에서 체크해뒀던 변경이 필요한 부분들을 Real DOM에 반영하는 단계이다.

  • 변경이 필요한 부분이 없다면, Commit Phase는 스킵한다.

▶️ useMemo, useCallback은 유지/ React.memo를 삭제한 후, React의 리렌더링 과정

  1. 부모 컴포넌트가 리렌더링 되고, 이후 자식 컴포넌트도 리렌더링 되면서 각각의 Render Phase가 실행된다.

  2. useMemo와 useCallback을 통해 자식 컴포넌트들의 props 값(참조)을 이전 props 값(참조)와 동일하게 유지한다.

  3. Rendar Phase내의 재조정 과정에서 Real DOM에 변경이 필요한 부분이 없다고 판단한다.
    따라서, Commit Phase는 실행되지 않는다.

  4. Commit Phase는 막았지만 Render Phase가 여전히 실행된다.

  5. 최적화를 하긴 했는데, 눈으로 보기에는 최적화를 진행하기 전과 거의 동일하게 동작한다. 😱

▶️ 결론: Render Phase까지 막기 위해 사용하는 것이 React.memo이다 !!


❓ useMemo, useCallback, React.memo를 많이 사용하면 좋을까?

  • React.memo는 컴포넌트 변경 여부를 검사하기 위해 매 렌더링마다 얕은 비교를 수행한다.

  • useMemo, useCallback 역시 매 렌더링 Phase마다 의존성 배열 검사 및 캐시 관리를 수행한다.

  • 이런 모든 것들이 비용이기 때문에, 리렌더링을 최적화한 효과보다 다른 부분에서 오히려 성능 저하를 일으킬 수 있다.

  • 결국.. 참조 동일성을 반드시 유지해야 하는 상황이더라도, 메모이제이션을 적용하는 것이 항상 정답이라고 말할 수는 없다.


3️⃣ 마무리

최적화에는 정답이 없다 ..! 최적화 기준을 잘 세우는 것, 그리고 단면만 보지 말고 전체적인 성능을 고려하여 최적의 선택을 하는 것이 중요하다.

어디선가, 아래와 같은 말을 들은 적이 있다. 정말 띵언

"최적화를 언제 하면 좋을까요??"
1. 하면 좋을 것 같아요. > 하지 마세요.
2. 쓰읍.. 해야할 것 같은데 .. > 한 번 더 생각해보세요.
3. 진짜 진짜 진짜 무조건 해야된다 !!! > 프로파일링(측정)부터 진행해보세요.

항상 모든 것에는 trade-off가 있기 때문에, 최적화를 해서 특정 시간을 줄였다면 결국 어떤 것 (메모리, 코드의 양, 개발자의 시간 등등)을 희생했다는 이야기가 된다.

결국 어떤 것을 희생할지 잘 생각해보고, 최적화로 인해 발생하는 불필요한 비용을 최소화하는 것이 중요하다.



📚 참고자료

메모이제이션을 활용한 리렌더링 최적화
[React] React.memo로 Rendering 최적화2 (feat.예제1,2)

0개의 댓글

관련 채용 정보