대체 왜 리렌더링 되는 거야? - useWhyDidYouUpdate

792025년 11월 02일4

애플리케이션이 복잡해지고, 성능 최적화가 중요한 과제로 떠오르면서 개발자들은 새로운 종류의 미스터리와 마주하기 시작했다.

“대체 이 컴포넌트는 왜 리렌더링 되는 거야?”

한 개발자가 성능 문제로 골머리를 앓고 있었다. 그는 불필요한 리렌더링을 막기 위해 자식 컴포넌트를 React.memo로 감쌌다. props가 바뀌지 않는 한, 리렌더링이 일어나지 않으리라 확신했다.

하지만 리액트 개발자 도구는 그의 믿음을 배신했다.
부모 컴포넌트의 사소한 상태 변경에도, React.memo로 감싸인 자식 컴포넌트는 여전히 번쩍이며 리렌더링되고 있었다.

그는 코드를 몇 번이고 다시 읽어봤다.
props로 전달되는 값들은 원시 값(primitive values)이었고, 함수들은 모두 useCallback으로 감싸여 있었다. 겉으로 보기에는 완벽한 최적화 코드였다.

“분명히 props는 변하지 않았는데… 뭔가 보이지 않는 곳에서 다른 일이 벌어지고 있는 게 틀림없어.”

그는 원인을 찾기 위해 console.log를 찍고, props 객체를 비교하는 코드를 수동으로 추가하며 몇 시간을 허비했다. 디버깅 과정은 고통스럽고 비효율적이었다.

이러한 개발자들의 고통을 해결하기 위해, 커뮤니티에서 기발한 아이디어의 커스텀 훅이 등장했다.
그 훅의 이름은 useWhyDidYouUpdate였다.

이 훅의 목적은 단 하나였다.
컴포넌트가 리렌더링될 때마다, 어떤 prop 혹은 state가 변경되어 리렌더링을 유발했는지 그 원인을 콘솔에 명확하게 알려주는 것.

그 내부 구현은 useEffectuseRef를 영리하게 조합한 형태였다.

import { useEffect, useRef } from 'react';

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changesObj = {};
      allKeys.forEach((key) => {
        // 이전 props와 현재 props를 비교한다.
        if (previousProps.current[key] !== props[key]) {
          changesObj[key] = {
            from: previousProps.current[key],
            to: props[key],
          };
        }
      });

      if (Object.keys(changesObj).length) {
        console.log('[Why Did You Update?]', name, changesObj);
      }
    }
    // 렌더링이 끝난 후, 현재 props를 이전 props로 저장한다.
    previousProps.current = props;
  });
}

이 디버깅용 커스텀 훅을 사용하는 것은 아주 간단했다.
문제가 되는 컴포넌트의 맨 위에 단 한 줄만 추가하면 되었다.

const MyComponent = React.memo((props) => {
  // 디버깅 훅 호출
  useWhyDidYouUpdate('MyComponent', props);

  return <div>...</div>;
});

이제, 컴포넌트가 리렌더링될 때마다 콘솔 창에는 놀라운 정보가 찍혔다.

[Why Did You Update?] MyComponent { style: { from: {color: 'blue'}, to: {color: 'blue'} } }

범인은 style prop이었다!
부모 컴포넌트가 style={{ color: 'blue' }}와 같이 인라인 스타일 객체를 prop으로 전달하고 있었던 것이다. 이 스타일 객체는 내용물은 같았지만, 렌더링될 때마다 새로운 메모리 주소를 가진 객체로 생성되어 React.memo의 비교를 통과하지 못했다.

useWhyDidYouUpdate는 마치 탐정처럼, 리렌더링의 숨겨진 원인을 정확하게 찾아내 개발자에게 알려주었다. 개발자는 더 이상 추측에 의존하여 시간을 낭비할 필요가 없었다.

이 커스텀 훅은 훅 패러다임이 가진 또 다른 가능성을 보여주었다.
훅은 단지 애플리케이션의 로직을 구성하는 데만 사용되는 것이 아니었다. 그것은 개발 과정 자체를 개선하고, 디버깅을 더 쉽고 효율적으로 만드는 강력한 ‘메타 프로그래밍’ 도구가 될 수도 있었다.

개발자들은 이제 useWhyDidYouUpdate와 같은 디버깅 훅을 자신들의 무기고에 추가했다.
보이지 않는 적, ‘불필요한 리렌더링’과의 싸움에서, 그들은 마침내 적의 위치를 정확하게 파악할 수 있는 레이더를 손에 넣은 셈이었다.