리액트 컴포넌트는 상태(State)나 프롭스(Props)가 변경될 때마다 다시 렌더링됩니다. 이때 컴포넌트 내부에서 선언된 함수들도 매번 새롭게 생성됩니다. useCallback은 이러한 함수 생성 과정을 캐싱(메모이제이션)하여, 불필요한 함수 재생성과 하위 컴포넌트의 리렌더링을 방지해주는 강력한 성능 최적화 Hook입니다.
🤔 왜 함수를 캐싱해야 할까요?
자바스크립트에서 함수는 객체(Object)입니다. 내용이 완전히 똑같더라도, 새로 만들어진 함수는 이전 함수와 메모리 주소가 다릅니다. 이 때문에 자식 컴포넌트에게 함수를 Props로 넘겨줄 때, 자식 입장에서는 매번 "새로운 프롭스가 들어왔네?"라고 착각하여 불필요하게 렌더링을 수행하게 됩니다.
📦 의존성 배열 (Dependency Array)
useCallback은 두 번째 인자로 의존성 배열을 받습니다. 이 배열 안에 있는 값이 변할 때만 함수를 새로 생성합니다. useEffect의 동작 방식과 완전히 동일합니다.
형태
설명
언제 함수가 새로 생성되나요?
[] (빈 배열)
최초 렌더링 시 한 번만 생성됨
절대 재생성되지 않음 (상태 업데이트 시 주의)
[a, b] (의존성 포함)
배열 안의 값이 바뀔 때만 생성됨
a 또는 b 값이 변경될 때
배열 생략
렌더링될 때마다 매번 생성됨
매 렌더링마다 (useCallback을 쓰는 의미가 없음)
⚠️ 주의: 언제 쓰지 말아야 할까요?
모든 함수를 무작정 useCallback으로 감싸면 오히려 독이 될 수 있습니다. 메모이제이션 자체도 비용(메모리 할당 및 비교 연산)이 들기 때문입니다.
👉 사용을 권장하는 경우: 자식 컴포넌트에 Props로 함수를 넘길 때 (특히 자식이 React.memo로 최적화되어 있을 때)
👉 권장하지 않는 경우: 단순한 계산이나 가벼운 컴포넌트 내부에서만 쓰이는 함수
SearchWithCallback.jsx
import React, { useState, useCallback, memo } from 'react';
// 엄청 무거운 렌더링이 발생하는 가상의 데이터 리스트
const MassiveList = memo(({ items, onItemClick }) => {
console.log('🔴 무거운 리스트 렌더링 발생!');
// 가상의 지연 시간 (렌더링을 무겁게 만듦)
const start = performance.now();
while (performance.now() - start < 300) {
// Artificial delay
}
return (