프로그래밍/ReactJS

React 성능 최적화의 비밀 병기: useCallback 훅 완벽 가이드

shimdh 2025. 10. 13. 14:01
728x90

React 애플리케이션이 점점 규모가 커지면서, 개발자들은 성능 최적화에 더 많은 시간을 투자하게 됩니다. 특히 불필요한 컴포넌트 재렌더링은 앱의 반응성을 떨어뜨리고, 사용자 경험(UX)을 해칩니다. 이런 문제를 해결하는 강력한 무기가 바로 useCallback 훅입니다. 이 글에서는 useCallback의 기본 개념부터 실전 적용 팁, 그리고 주의할 점까지 자세히 탐구해보겠습니다. 초보자부터 중급 개발자까지, React 성능 튜닝의 필수 지식을 챙겨보세요!

728x90

useCallback이란 무엇인가요?

useCallback 훅은 React에서 함수를 메모이제이션(memoization) 하여 성능을 최적화하는 도구입니다. 메모이제이션이란? 간단히 말해, 이전에 계산한 결과를 메모리에 저장해두고, 입력값이 같으면 다시 계산하지 않고 저장된 값을 재사용하는 기법입니다. 컴퓨터 과학에서 자주 쓰이는 이 개념을 React에서 함수에 적용한 게 바로 useCallback입니다.

이 훅은 콜백 함수의 메모이제이션된 버전을 반환합니다. 함수는 의존성 배열(dependency array) 에 포함된 값 중 하나라도 변경될 때만 새로 생성됩니다. 왜 중요한가요? React는 참조 동일성(reference equality)을 기반으로 props 변화를 감지하는데, 매 렌더링마다 새 함수가 생성되면 자식 컴포넌트가 불필요하게 재렌더링될 수 있습니다. useCallback은 이런 문제를 해결해줍니다.

예를 들어, 부모 컴포넌트에서 자식에게 이벤트 핸들러를 props로 넘길 때, useCallback으로 감싸면 함수 참조가 안정적으로 유지되어 최적화 효과가 큽니다.

useCallback을 사용해야 하는 이유

useCallback을 왜 써야 할까요? 성능 이득이 크기 때문입니다. 주요 이유를 두 가지로 정리해보죠.

1. 불필요한 재렌더링 방지

React 함수 컴포넌트가 렌더링될 때마다 내부 함수는 항상 새로 생성됩니다. 로직이 바뀌지 않았어도 새로운 참조가 생기죠. 이 함수를 props로 자식에게 넘기면, 자식 컴포넌트가 props 변화로 재렌더링됩니다. 결과? 불필요한 DOM 업데이트와 CPU 낭비!

useCallback은 함수를 메모이제이션해 참조를 유지합니다. 덕분에 재렌더링이 최소화되고, 앱이 더 부드럽게 동작합니다. 특히 대규모 리스트나 반복 컴포넌트에서 빛을 발합니다.

2. 자식 컴포넌트 최적화

React.memo를 아시죠? props가 바뀌지 않으면 재렌더링을 스킵하는 HOC(Higher-Order Component)입니다. 하지만 함수 props가 매번 새로 생성되면 React.memo가 제대로 작동하지 않습니다.

useCallback을 쓰면 함수 props의 참조가 안정적이기 때문에, React.memo가 의도대로 동작합니다. 예를 들어, 리스트 아이템 컴포넌트가 수백 개라면 이 조합으로 렌더링 비용을 50% 이상 줄일 수 있습니다.

useCallback 기본 문법

useCallback의 문법은 간단합니다. useCallback을 import하고, 함수와 의존성 배열을 넘기세요.

import React, { useCallback } from 'react';

const MyComponent = () => {
  const memoizedCallback = useCallback(() => {
    // 여기에 함수 로직 작성 (예: API 호출, 상태 업데이트 등)
    console.log('함수가 실행되었습니다!');
  }, [dependency1, dependency2]); // 의존성 배열: 이 값들이 바뀔 때만 함수 재생성

  return (
    <div>
      <button onClick={memoizedCallback}>클릭하세요!</button>
    </div>
  );
};
  • 첫 번째 인수: 메모이제이션할 함수.
  • 두 번째 인수: 의존성 배열. 빈 배열 []이면 컴포넌트 마운트 시 한 번만 생성됩니다. (주의: ESLint의 react-hooks/exhaustive-deps 규칙을 따르세요!)
  • : 의존성이 많으면 useMemo와 결합해 더 복잡한 값을 메모이제이션할 수도 있습니다.

실용적인 예시: 카운터 애플리케이션

이론은 그만! 간단한 카운터 앱으로 useCallback의 마법을 느껴보죠. Increment/Decrement 버튼이 있는 앱입니다. 버튼 컴포넌트에 React.memo를 적용했지만, 최적화 전후를 비교해보세요.

useCallback 적용 전 코드

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(prevCount => prevCount + 1); // 매 렌더링마다 새 함수!
  const decrement = () => setCount(prevCount => prevCount - 1); // 매 렌더링마다 새 함수!

  return (
    <div>
      <h1>{count}</h1>
      <Button onClick={increment}>Increment</Button>
      <Button onClick={decrement}>Decrement</Button>
    </div>
  );
};

const Button = React.memo(({ onClick, children }) => {
  console.log(`Rendering button: ${children}`); // 디버깅용 로그

  return (
    <button onClick={onClick}>{children}</button>
  );
});

문제점: Counter가 재렌더링될 때마다 incrementdecrement가 새로 생성됩니다. Button의 onClick props가 바뀌니 React.memo에도 불구하고 버튼이 매번 재렌더링! 콘솔에 "Rendering button: Increment"가 반복 출력됩니다.

useCallback을 사용하여 최적화된 코드

import React, { useState, useCallback } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => setCount(prevCount => prevCount + 1), []); // 빈 배열: 한 번만 생성
  const decrement = useCallback(() => setCount(prevCount => prevCount - 1), []); // 빈 배열: 한 번만 생성

  return (
    <div>
      <h1>{count}</h1>
      <Button onClick={increment}>Increment</Button>
      <Button onClick={decrement}>Decrement</Button>
    </div>
  );
};

const Button = React.memo(({ onClick, children }) => {
  console.log(`Rendering button: ${children}`); // 이제 처음 한 번만 출력!

  return (
    <button onClick={onClick}>{children}</button>
  );
});

효과: 함수 참조가 유지되니 Button이 재렌더링되지 않습니다. 콘솔 로그가 처음 한 번만 나오고, 앱이 더 효율적입니다. 실제 프로젝트에서 이런 작은 변화가 누적되어 큰 성능 향상을 가져옵니다.

추가 팁: 만약 setCount 외에 다른 상태(예: userId)에 의존한다면 의존성 배열에 [userId]를 넣으세요. 함수가 userId 변화 시에만 업데이트됩니다.

useCallback, 언제 사용해야 할까요?

useCallback은 만능이 아닙니다. 남용하면 코드가 복잡해지고, 메모리 오버헤드(함수 저장 비용)가 생길 수 있습니다. 다음 가이드라인을 따르세요:

  • 성능 병목 지점에만 사용: 프로파일러(React DevTools)로 재렌더링을 확인한 후 적용하세요. 사소한 함수에는 과도하게 쓰지 마세요.
  • props로 전달되는 함수 우선: 특히 React.memouseEffect 의존성에 쓰일 때 효과적입니다.
  • 의존성 배열 관리 철저히: 누락되면 버그(오래된 값 사용), 과도하면 재생성 빈도 증가. ESLint 규칙을 활용해 자동 체크하세요.
  • 대안 고려: 간단한 경우 useMemo로 값 메모이제이션, 또는 컴포넌트 분리.

이 지침으로 useCallback을 현명하게 쓰면, 코드 품질과 성능이 동시에 올라갑니다.

결론

useCallback은 React 성능 최적화의 핵심입니다. 불필요한 함수 재생성을 막아 재렌더링을 줄이고, React.memo와의 시너지를 발휘합니다. 대규모 앱에서 리소스를 아끼고 UX를 업그레이드하세요. 이 가이드를 통해 useCallback을 마스터하시고, 더 빠른 React 앱을 만들어 보세요.

728x90