useCallback으로 함수 기억하기

632025년 10월 17일3

“우리에겐 함수를 ‘기억’할 방법이 필요합니다. 리렌더링이 일어나도, 특정 함수는 이전 렌더링 때 만들었던 바로 그 함수를 재사용하도록 말이죠.”

소피는 불필요한 리렌더링 문제로 골머리를 앓는 팀에게, 새로운 훅 useCallback을 소개했다.

useCallback은 이름 그대로, 콜백(callback) 함수를 메모이제이션(memoization)하는, 즉 한 번 계산된 결과를 저장해두었다가 재사용하는 데 특화된 훅이었다.

그녀는 Dashboard 컴포넌트의 handleDeleteLog 함수를 useCallback으로 감쌌다.

function Dashboard() {
  const [logs, setLogs] = useState([]);
  const [filter, setFilter] = useState('all');

  // handleDeleteLog 함수를 useCallback으로 감싼다.
  const handleDeleteLog = useCallback((logId) => {
    setLogs((currentLogs) => currentLogs.filter((log) => log.id !== logId));
  }, []); // 의존성 배열은 일단 비워둔다.

  // ... (이하 동일)
}

useCallback은 두 개의 인자를 받았다. 첫 번째는 메모이제이션할 함수, 두 번째는 useEffect와 똑같이 작동하는 의존성 배열이었다.

소피가 그 동작 원리를 설명했다.
useCallback은 리액트에게 이렇게 말하는 겁니다. ‘내가 전달한 이 함수를 기억해둬. 그리고 다음 렌더링 때, 이 의존성 배열의 값이 바뀌지 않는 한, 새로운 함수를 만들지 말고 이전에 기억해뒀던 바로 그 함수를 그대로 돌려줘.’”

이 예제에서는 의존성 배열이 비어있었다. [].
이것은 handleDeleteLog 함수가 외부의 어떤 값에도 의존하지 않으므로, 애플리케이션의 생애 동안 단 한 번만 생성되고 영원히 재사용될 것임을 의미했다.

이제, Dashboardfilter 상태가 바뀌어 리렌더링이 일어나도, useCallback은 새로운 함수를 만들지 않았다. 그저 이전에 만들었던 handleDeleteLog 함수의 참조를 그대로 반환했다.

React.memo로 감싸인 <LogItem> 컴포넌트는 이제 행복해졌다.
log prop도 그대로, onDelete prop으로 전달된 함수도 이전 렌더링과 똑같은 참조를 가진 바로 그 함수였다. props가 변경되지 않았다고 판단한 React.memo는 마침내 자신의 역할을 수행하여, 불필요한 리렌더링을 건너뛰었다.

개발자 도구의 화면에서, 번쩍이던 하이라이트가 마법처럼 사라졌다.

useCallbackReact.memo와 짝을 이루어, 렌더링 최적화의 강력한 한 축을 담당했다.
하지만 이것은 만병통치약이 아니었다.

한 개발자가 예리한 질문을 던졌다.
“만약 handleDeleteLog 함수가 외부 변수인 filter 상태를 사용한다면 어떻게 되죠? 의존성 배열을 비워두면, 함수는 항상 오래된 filter 값을 기억하고 있는 것 아닌가요?”

정확한 지적이었다. useEffect와 마찬가지로, useCallback도 의존성 관리가 핵심이었다.

const handleDeleteLog = useCallback(
  (logId) => {
    // 만약 filter 값을 사용한다면...
    console.log(`Deleting log with filter: ${filter}`);
    setLogs((currentLogs) => currentLogs.filter((log) => log.id !== logId));
  },
  [filter]
); // 반드시 의존성 배열에 'filter'를 추가해야 한다.

filter를 의존성 배열에 추가하면, filter 상태가 바뀔 때만 useCallback은 새로운 handleDeleteLog 함수를 생성한다. 이것은 의도된 동작이었다.

useCallback은 개발자에게 새로운 종류의 트레이드오프를 제시했다.
성능 최적화를 위해 함수 재생성을 막는 편리함을 얻는 대신, 함수가 의존하는 모든 값을 정확하게 추적하고 관리해야 하는 책임을 져야 했다.

다행히, 그들의 충실한 파수꾼 eslint-plugin-react-hooksuseCallback의 의존성 배열 역시 철저하게 검사해주었다.

함수를 기억하는 문제는 해결되었다.
하지만 아직 한 가지 문제가 더 남아있었다. 함수가 아닌, 복잡한 ‘계산 결과 값’을 기억해야 하는 경우는 어떻게 해야 할까?
팀의 시선은 useCallback의 쌍둥이와도 같은, 또 다른 최적화 훅을 향하고 있었다.