최적화는 공짜가 아니다

652025년 10월 19일3

useCallbackuseMemo라는 새로운 무기를 손에 쥔 개발자들 사이에서, 새로운 종류의 안티패턴(anti-pattern)이 고개를 들기 시작했다. 그것은 ‘성급한 최적화(premature optimization)’라는 이름의 오랜 악습이었다.

어떤 개발자들은 이 훅들의 힘에 매료된 나머지, 애플리케이션의 모든 함수를 useCallback으로, 모든 계산된 값을 useMemo로 감싸기 시작했다. 그들은 이것이 애플리케이션의 성능을 무조건적으로 향상시킬 것이라고 믿었다.

댄은 페이스북의 코드 리뷰 과정에서 그런 코드를 발견했다.

function UserProfile({ user }) {
  const fullName = useMemo(() => `${user.firstName} ${user.lastName}`, [user]);
  
  const handleClick = useCallback(() => {
    console.log(`Clicked on ${fullName}`);
  }, [fullName]);

  return (
    <div onClick={handleClick}>
      <p>{fullName}</p>
    </div>
  );
}

이 코드는 기술적으로는 문제가 없었다. 하지만 댄은 이 코드가 오히려 불필요한 복잡성과 미세한 성능 저하를 유발하고 있음을 간파했다.

그는 코드 리뷰 코멘트를 통해 개발자에게 조언했다.
“이 최적화가 정말로 필요한지 다시 한번 생각해 볼 필요가 있습니다.”

그의 설명이 이어졌다.
useMemouseCallback 자체도 공짜가 아닙니다. 이 훅들은 리액트가 이전 렌더링의 값이나 함수를 저장하기 위해 추가적인 메모리를 사용하게 합니다. 또한, 매 렌더링마다 의존성 배열을 비교하는 계산 비용도 발생하죠.”

UserProfile 컴포넌트의 경우, fullName을 계산하는 것은 단순히 두 문자열을 합치는, 아주 ‘저렴한’ 연산이었다. 이 연산을 useMemo로 감싸서 얻는 이득은 거의 없거나, 오히려 훅 자체의 비용 때문에 손해일 수도 있었다.

handleClick 함수 역시 마찬가지였다. 이 함수가 props로 다른 자식 컴포넌트에 전달되고, 그 자식 컴포넌트가 React.memo로 감싸여 있지 않다면, useCallback을 사용하는 것은 아무런 의미가 없었다. 어차피 자식은 부모가 리렌더링될 때마다 함께 리렌더링될 것이기 때문이다.

댄은 컴퓨터 과학의 오랜 격언을 인용했다.
“‘섣부른 최적화는 모든 악의 근원이다(Premature optimization is the root of all evil).’”

그는 팀에 새로운 가이드라인을 제시했다.
useCallbackuseMemo는 일반적인 상황에서는 사용할 필요가 없습니다. 이 훅들은 특정한 문제를 해결하기 위한 ‘수술 도구’와 같습니다. 아무 때나 남용해서는 안 됩니다.”

그가 제시한 ‘수술이 필요한 경우’는 명확했다.

  1. useCallback: 함수를 prop으로 받는 자식 컴포넌트가 React.memo를 통해 최적화되어 있을 때, 그 자식의 불필요한 리렌더링을 막기 위해 사용한다.
  2. useMemo: 정말로 계산 비용이 비싼 연산(예: 거대한 배열의 필터링 및 정렬)의 결과를 캐싱하여, 불필요한 재계산을 피하고 싶을 때 사용한다.

“가장 중요한 원칙은 이것입니다. 먼저, 최적화 없이 코드를 작성하세요. 그리고 만약 애플리케이션에서 실제로 성능 병목 현상이 발생한다면, 리액트 개발자 도구의 프로파일러(Profiler)를 사용해 문제의 원인이 되는 컴포넌트를 정확히 찾아내세요. 그리고 오직 그곳에만, 이 수술 도구들을 사용하십시오.”

이 가이드라인은 개발자들이 최적화 훅을 맹목적으로 사용하는 것을 막고, 이성적으로 접근하도록 이끌었다. 최적화는 언제나 비용과 이득을 따져봐야 하는 트레이드오프의 문제임을 명확히 한 것이다.

이로써 훅을 사용한 성능 최적화에 대한 논의까지 어느 정도 마무리되었다.
리액트 팀은 이제, 훅의 무기고에 남아있는 마지막 몇 개의 특수한 도구들을 살펴보기 시작했다. 그중 하나는 클래스 시절의 createRef를 대체하고, 더 나아가 훅 패러다임에서 아주 독특한 역할을 수행하게 될, 특별한 훅이었다.