useCallback: 함수를 캐싱하는 방법

useCallback은 함수를 메모이제이션합니다. 의존성이 바뀌지 않으면 매 렌더링마다 새 함수를 만드는 대신, 이전에 만든 함수를 재사용합니다. useMemo의 함수 버전이라고 생각하면 됩니다.

기본 사용법

import { useCallback } from 'react';
 
const cachedFn = useCallback(() => {
  // 함수 내용
}, [의존성]);

실제로 useCallback(fn, deps)useMemo(() => fn, deps)와 동일합니다.

왜 필요한가?

React 컴포넌트가 렌더링될 때마다 그 안에 정의된 함수들은 새로 생성됩니다. 내용이 같아도 새 함수이므로 참조가 다릅니다.

function Parent() {
  const [count, setCount] = useState(0);
 
  // 매 렌더링마다 새 함수가 만들어짐
  const handleClick = () => {
    console.log('클릭!');
  };
 
  return <Child onClick={handleClick} />;
}

ChildReact.memo로 감싸져 있어도, handleClick의 참조가 매번 바뀌므로 Child는 매 렌더링마다 다시 렌더링됩니다. useCallback으로 이를 막을 수 있습니다.

function Parent() {
  const [count, setCount] = useState(0);
 
  // count가 바뀌지 않으면 같은 함수 참조를 유지
  const handleClick = useCallback(() => {
    console.log('클릭!');
  }, []);
 
  return <Child onClick={handleClick} />;
}

의존성이 있는 경우

함수 내부에서 상태나 props를 참조한다면, 의존성 배열에 포함해야 합니다.

function SearchForm({ onSearch }) {
  const [query, setQuery] = useState('');
 
  const handleSubmit = useCallback(() => {
    onSearch(query); // query를 사용하므로 의존성에 포함
  }, [query, onSearch]);
 
  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button type="submit">검색</button>
    </form>
  );
}

useMemo와의 차이

useMemouseCallback
캐싱 대상함수
반환fn() 실행 결과fn 자체
필터링된 배열이벤트 핸들러
// 값을 캐싱
const sortedList = useMemo(() => [...list].sort(), [list]);
 
// 함수를 캐싱
const handleSort = useCallback(() => {
  setSortedList([...list].sort());
}, [list]);

언제 써야 할까?

useCallbackuseMemo와 마찬가지로 남용하면 오히려 성능이 나빠집니다. 메모이제이션 자체도 비용(메모리, 비교 연산)이 있기 때문입니다.

사용이 적합한 경우

  • React.memo로 감싼 자식 컴포넌트에 함수를 prop으로 전달할 때
  • useEffect의 의존성으로 함수를 넣어야 할 때

불필요한 경우

  • 자식이 React.memo로 감싸지 않은 경우 (어차피 리렌더링됨)
  • 단순한 이벤트 핸들러를 같은 컴포넌트 내에서만 사용하는 경우

정리

  • useCallback은 함수의 참조를 안정적으로 유지합니다
  • 내부에서 사용하는 값은 반드시 의존성 배열에 포함해야 합니다
  • React.memo와 함께 사용할 때 가장 효과적입니다
  • 성능 문제가 실제로 발생하기 전까지는 무작정 추가하지 마세요