useCallback은 종속성이 변경될 때까지 렌더링 간에 변경되지 않는 함수의 메모이제이션된 버전을 반환하는 React 후크입니다. 컴포넌트 내부의 일반 함수 선언(렌더링마다 새 함수를 생성)과 달리, useCallback은 함수 참조를 안정화하여 React.memo로 최적화된 자식 컴포넌트의 불필요한 리렌더링을 방지합니다. React Documentation (2025)에 따르면, useCallback은 React.memo 또는 안정적인 참조에 의존하는 후크와 함께 사용할 때만 유용합니다.
핵심 포인트
useCallback은 React 16.8에 추가된 후크로, 함수를 메모이제이션합니다: 종속성이 변경될 때까지 동일한 참조를 반환합니다. useCallback이 없으면, 컴포넌트 내부의 함수 선언은 렌더링마다 새로운 함수 객체를 생성합니다. 프리미티브의 경우 눈에 띄지 않지만, React.memo로 최적화된 자식 컴포넌트에 이러한 콜백을 전달할 때, 각각의 새 참조는 자식 컴포넌트의 리렌더링을 유발합니다.
구문적으로, useCallback은 함수에 대한 useMemo와 동일합니다: useCallback(fn, deps)은 useMemo(() => fn, deps)의 약어입니다. React는 메모이제이션된 함수를 파이버 노드의 내부 저장소에 보관하고 각 렌더링에서 종속성을 비교합니다. 종속성이 변경되지 않은 경우(각 항목에 대해 Object.is), 이전 함수가 반환됩니다.
React Documentation — useCallback (2025)에 따르면, 모든 함수를 useCallback으로 감싸면 안 됩니다. 후크에는 자체 비용이 있습니다: 후크 호출, 종속성 비교 및 종속성 배열에 대한 메모리 할당입니다. 컴포넌트가 간단하고 React.memo와 함께 깊은 트리가 없는 경우, useCallback은 애플리케이션을 느리게 할 뿐입니다. 최적화는 측정 가능해야 하며 직관적이지 않아야 합니다.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 안정적인 참조 — deps가 변경될 때까지 동일한 함수
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
useCallback의 메모이제이션은 함수 호출 결과를 캐싱하는 데 기반합니다. React는 첫 번째 렌더링 시 생성된 클로저를 보존하고 종속성이 변경되지 않는 한 후속 렌더링에서 이를 반환합니다. 파이버 노드 내부에서, 각 useCallback 호출은 후크의 연결 리스트에 노드를 생성하며, 이전 종속성과 메모이제이션된 값이 저장됩니다.
종속성 비교는 엄격하게 Object.is를 통해 수행됩니다 — 객체나 배열의 깊은 검사 없이 얕은 비교입니다. 종속성이 객체나 배열인 경우, 각 렌더링의 새 참조는 변경으로 간주됩니다. 따라서 종속성 배열에는 프리미티브 값이나 안정적인 참조(예: useRef 또는 useMemo에서)가 포함되어야 합니다.
React Core Team — Optimization Guide (2024)에 따르면, 메모이제이션 비용에는 세 가지 구성 요소가 포함됩니다: 각 렌더링에서 종속성 배열 할당, Object.is를 통한 요소 반복 및 비교, 재생성 시 가비지 컬렉션 오버헤드입니다. 수백 개의 useCallback 래퍼가 있는 컴포넌트의 경우, 이는 눈에 띌 수 있습니다 — 따라서 후크 사용 시 선택성이 중요합니다.
| 시나리오 | useCallback 없음 | useCallback 사용 |
|---|---|---|
| 함수 생성 | 렌더링마다 새로 생성 | 안정적인 deps에서 동일 |
| React.memo에 전달 | 자식이 리렌더링됨 | 자식이 리렌더링되지 않음 |
| useEffect 배열 내 | 효과가 다시 시작됨 | 효과가 안정적임 |
| 오버헤드 | 최소 | 종속성 비교 + 메모리 |
useCallback이 자동으로 성능을 향상시킨다는 널리 퍼진 오해가 있습니다. 실제로, 단독으로(React.memo 없이) useCallback은 종속성 비교 비용으로 인해 애플리케이션을 약간 느리게 만들기도 합니다. 후크는 세 가지 시나리오에서만 실제 이점을 제공합니다: React.memo 컴포넌트의 리렌더링 방지, useEffect에서 콜백 안정화, 참조 동등성에 의존하는 사용자 정의 후크에 콜백 전달.
규칙은 간단합니다: React DevTools Profiler를 통해 성능 문제를 발견할 때까지 useCallback을 사용하지 마십시오. React 팀은 조기 최적화가 모든 악의 근원이라고 반복해서 강조했습니다. 먼저 메모이제이션 없이 깨끗한 코드를 작성하고, 측정하고, 프로파일러에서 병목 현상을 찾은 다음, 정말로 필요한 곳에 useCallback을 추가하십시오.
// 측정 가능한 최적화: Child가 React.memo로 감싸져 있음
const Child = React.memo(({ onClick }) => {
console.log('Child가 리렌더링됨');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('클릭됨');
}, []);
return <Child onClick={handleClick} />;
}
Dan Abramov — Before You memo() (2024)에 따르면, 오픈 소스 프로젝트에서 useCallback 사용 사례의 90% 이상이 중복됩니다. 개발자는 효과를 측정하지 않고 "만약을 대비해" 모든 함수를 감쌉니다. 대안: 자식 컴포넌트가 무겁고 리렌더링 비용이 많이 드는 경우 — React.memo + useCallback이 정당화됩니다. 자식 컴포넌트가 가벼운 경우 — 리렌더링이 종속성 비교보다 저렴합니다.
첫 번째 시나리오는 React.memo입니다. 자식 컴포넌트가 React.memo로 감싸져 있고 props로 콜백 함수를 받는 경우, useCallback이 없으면 자식 컴포넌트는 자체 데이터가 변경되지 않았더라도 모든 부모 렌더링에서 리렌더링됩니다. useCallback은 참조를 안정화하여 React.memo가 리렌더링을 올바르게 건너뛸 수 있게 합니다.
두 번째 시나리오는 종속성에 콜백이 있는 useEffect입니다. 함수가 useEffect의 종속성 배열에 전달되면, 각각의 새 참조가 효과를 다시 시작합니다. useCallback은 참조가 안정적이고 효과가 실제 데이터가 변경될 때만 실행되도록 보장하며, 모든 렌더링에서 실행되지 않습니다. 이는 구독 및 요청에 특히 중요합니다.
// 안정적인 useEffect 종속성을 위한 useCallback
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // 안정적인 참조, 다시 생성되지 않음
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // props.id가 변경될 때만 효과 실행
useCallback과 useMemo의 주요 차이점은 각각이 무엇을 메모이제이션하는지입니다. useCallback은 함수를 메모이제이션합니다: useCallback(fn, deps)은 fn(동일하거나 이전 버전)을 반환합니다. useMemo는 함수 호출 결과를 메모이제이션합니다: useMemo(() => computeExpensive(a, b), [a, b])는 계산된 값을 반환하며 함수가 아닙니다.
기술적으로, useCallback은 useMemo 위의 구문 설탕입니다: useCallback(fn, deps)은 useMemo(() => fn, deps)와 동일합니다. 이 구문은 가독성을 위해서만 존재합니다 — 개발자가 값이 아닌 함수가 메모이제이션되고 있음을 명확히 볼 수 있도록 하기 위해서입니다. useCallback과 함수와 함께 사용하는 useMemo 사이에는 성능 차이가 없습니다 — 동일한 코드를 생성합니다.
| 후크 | 메모이제이션 대상 | 구문 | 용도 |
|---|---|---|---|
| useCallback | 함수(참조) | useCallback(fn, deps) | 자식 컴포넌트용 콜백 |
| useMemo | 계산 결과 | useMemo(() => value, deps) | 비용이 많이 드는 계산, 객체 메모이제이션 |
// 이것들은 동일합니다:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
가장 흔한 실수는 자식 컴포넌트에 React.memo 없이 모든 함수를 useCallback으로 무의미하게 감싸는 것입니다. 자식 컴포넌트가 React.memo로 감싸져 있지 않은 경우, 콜백 참조가 변경되는지 여부와 관계없이 모든 부모 렌더링에서 계속 리렌더링됩니다. React.memo 없는 useCallback은 이익 없는 비용입니다.
// ❌ 무용: 자식에 React.memo 없음
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child가 React.memo 없이도 계속 리렌더링됨
// ❌ 스테일 클로저: 종속성 누락
const handle = useCallback(() => {
console.log(count); // count는 항상 0 — 스테일 클로저!
}, []);
// ✅ 올바름: 종속성 포함
const handle = useCallback(() => {
console.log(count);
}, [count]);
useCallback의 스테일 클로저 문제는 사용된 모든 변수를 종속성 배열에 포함하여 해결됩니다. exhaustive-deps와 함께 eslint-plugin-react-hooks는 콜백 본문의 모든 변수가 배열에 있는지 자동으로 확인합니다. 콜백이 렌더링 간에 변경되지 않는 setState를 사용하는 경우, 안전하게 deps에 포함할 수 있습니다 — React는 setState의 안정성을 보장합니다.
자주 묻는 질문
아니요. useCallback은 세 가지 경우에만 의미가 있습니다: 자식 컴포넌트가 React.memo로 감싸져 있거나, 함수가 useEffect 종속성 배열에서 사용되거나, 함수가 참조 동등성에 의존하는 사용자 정의 후크에 전달되는 경우입니다. 다른 경우에는 useCallback이 이익 없이 오버헤드를 추가합니다. React 팀은 먼저 최적화 없이 작성하고 프로파일링 결과에 따라 추가할 것을 권장합니다.
단순한 컴포넌트의 경우 — 렌더링마다 새 함수가 약간 더 빠릅니다. useCallback이 종속성 비교와 배열 할당에 리소스를 소비하기 때문입니다. 깊은 React.memo 트리가 있는 컴포넌트의 경우, useCallback이 수천 개의 자식 요소 리렌더링을 방지하여 더 좋습니다. 추측 대신 측정하고 비교하십시오 — 객관적인 평가를 위해 React DevTools Profiler를 사용하십시오.
네, useCallback은 async 함수와 동기 함수와 똑같이 작동합니다. 후크는 함수 자체를 메모이제이션하고, 결과(Promise)는 호출될 때마다 반환됩니다. useCallback 내부의 async 함수는 useEffect에서 사용되는 안정적인 데이터 로딩 콜백의 일반적인 패턴입니다: const fetchData = useCallback(async (id) => {...}, []).
React DevTools Profiler를 사용하십시오 — 어떤 컴포넌트가 리렌더링되는지와 그 이유를 보여줍니다. 프로그래매틱 확인을 위해, console.log를 추가하거나 useWhyDidYouUpdate(리렌더링 원인을 로깅하는 라이브러리)를 사용하십시오. 주요 원인: props 변경(콜백 참조 포함), 상태 변경, 또는 컨텍스트 변경입니다. useCallback이 도움이 되지 않는 경우 — 모든 종속성이 올바르게 지정되었는지 확인하십시오.
React.memo가 없어도, useCallback은 컨텍스트 값에 useMemo와 함께 유용할 수 있습니다. Context.Provider에 함수가 있는 객체를 전달하는 경우, 객체 생성을 useMemo로, 각 함수를 useCallback으로 감싸십시오. 이렇게 하면 함수 중 하나가 변경될 때 모든 컨텍스트 소비자의 리렌더링을 방지할 수 있습니다. 그러나 React.memo 없이 props에 직접 콜백을 전달하는 경우, useCallback의 이점이 없습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.