c9u11

useCallback 완벽 정리: 언제 쓰고, 언제 쓰지 말아야 할까

요약

useCallback함수의 참조(reference)를 캐싱하여 불필요한 리렌더링을 방지하는 React Hook이다.

주 용도는 함수를 props로 전달받는 자식 컴포넌트의 리렌더링을 막는 것이며, 모든 상황에서 사용하는 최적화 도구는 아니다. 잘못 사용하면 오히려 코드 가독성과 유지보수성을 해친다.


정의

useCallback은 다음과 같은 형태를 가진다.

const memoizedFn = useCallback(fn, dependencies);
  • 첫 번째 인자: 캐싱할 함수
  • 두 번째 인자: 의존성 배열(dependencies)
  • 의존성 배열의 값이 이전 렌더링과 Object.is 알고리즘 기준으로 동일하면
  • 하나라도 다르면

핵심은 **“함수 실행 결과가 아니라, 함수 자체를 메모이제이션한다”**는 점이다.


예시

❌ useCallback을 사용하지 않은 경우

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log('clicked');
  };

  return (
    <>
      <button onClick={() => setCount(count + 1)}>+</button>
      <Child onClick={handleClick} />
    </>
  );
}

const Child = React.memo(({ onClick }) => {
  console.log('Child render');
  return <button onClick={onClick}>Child Button</button>;
});
  • Parent가 리렌더링될 때마다 handleClick새 함수
  • ChildReact.memo로 감싸져 있어도

✅ useCallback을 사용한 경우

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>+</button>
      <Child onClick={handleClick} />
    </>
  );
}
  • handleClick의 참조가 유지됨
  • Child는 불필요한 리렌더링을 하지 않음

useMemo vs useCallback

두 Hook은 본질적으로 같은 개념이다.

useCallback(fn, deps)
// ===
useMemo(() => fn, deps);
  • useMemo값(value) 캐싱
  • useCallback함수(function) 캐싱

의미적으로 함수 캐싱임을 드러내기 위해 useCallback이 제공될 뿐이다.


장단점

장점

  • 함수 props로 인한 불필요한 자식 컴포넌트 리렌더링 방지
  • React.memo와 함께 사용할 때 효과적
  • 의존성 관리가 명확한 경우 성능 안정성 확보

단점

  • 무분별한 사용 시 가독성 저하
  • 의존성 배열 관리 실수 → 버그 발생
  • 실제 성능 병목이 아닌 곳에 쓰면 의미 없는 최적화

불필요한 메모이제이션을 없애는 5가지 원칙 (예시 포함)

1. 컴포넌트가 시각적으로 감싸고 있다면 JSX를 자식으로 받기

// ❌
<Layout>
  <Page />
</Layout>

// ✅
<Layout>
  {children}
</Layout>
  • 부모가 리렌더링되어도 children은 그대로 유지됨
  • 콜백 메모이제이션 필요성 감소

2. 가능한 한 로컬 state를 선호하기

// ❌ 전역 상태로 관리
const [isOpen, setIsOpen] = useGlobalStore();

// ✅ 필요한 컴포넌트 내부에서만 관리
const [isOpen, setIsOpen] = useState(false);
  • 상태 공유가 줄어들수록 리렌더링 전파도 줄어듦

3. 렌더링 로직 자체를 최적화하기

“컴포넌트 리렌더링이 눈에 띄는 문제를 만든다면,
// ❌ 렌더링 시 무거운 연산
const result = expensiveCalculation(data);

// ✅ 메모이제이션 혹은 구조 개선
const result = useMemo(() => expensiveCalculation(data), [data]);

4. state를 업데이트하는 불필요한 Effect 피하기

// ❌
useEffect(() => {
  setValue(a + b);
}, [a, b]);

// ✅
const value = a + b;
  • Effect는 외부 시스템과의 동기화 용도
  • 파생 상태는 렌더링 중 계산이 원칙

5. Effect에서 불필요한 의존성 제거하기

// ❌
useEffect(() => {
  doSomething();
}, [handler]);

// ✅
useEffect(() => {
  const handler = () => doSomething();
  handler();
}, []);
  • 함수 의존성 때문에 Effect가 자주 실행되는 경우가 많음

useCallback에서 state 의존성 제거하는 패턴

❌ state를 직접 참조하는 경우

const increment = useCallback(() => {
  setCount(count + 1);
}, [count]);

✅ 함수형 업데이트 사용

const increment = useCallback(() => {
  setCount(prev => prev + 1);
}, []);
  • setState항상 동일한 참조
  • state 의존성을 제거하여 콜백 재생성 방지

useEffect와 useCallback의 관계

  • useEffect가 자주 실행되는 원인 중 하나는 함수 의존성
  • 함수는 가급적 Effect 내부에서 정의
  • 외부에서 정의해야 한다면, 정말 필요한 경우에만 useCallback 사용

useCallback vs React.memo

  • useCallback함수 참조 안정화
  • React.memo컴포넌트 자체 캐싱
const Child = React.memo(function Child({ onClick }) {
  return <button onClick={onClick}>Click</button>;
});

👉 많은 경우, useCallback보다 React.memo가 더 큰 효과를 낸다.


Object.is 알고리즘 설명

React는 의존성 비교 시 **Object.is**를 사용한다.

Object.is 특징

Object.is(NaN, NaN);           // true
Object.is(+0, -0);             // false
Object.is({}, {});             // false (참조 비교)
Object.is(fn1, fn2);           // false
  • ===보다 더 정확한 비교
  • 참조 타입은 주소 비교
  • 그래서 객체·함수는 매 렌더링마다 새로 생성되면 “변경됨”으로 인식

👉 이 때문에 useCallback, useMemo가 필요한 상황이 생긴다.


자주 하는 실수 & 좋은 팁

❌ 실수

  • 모든 함수에 useCallback 남용
  • 의존성 배열을 비우기 위해 ESLint 경고 무시
  • 성능 이슈 없이 “습관적으로” 사용

✅ 좋은 팁

  • 리렌더링이 실제 문제인지 먼저 확인
  • 함수 props + React.memo 조합일 때만 고려