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은 새 함수Child는React.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조합일 때만 고려