프레임워크와 라이브러리/React

[useCallback] 불필요한 리렌더링 줄이기

Dachaes 2025. 4. 18. 13:51

useCallback 

React를 사용하다 보면 컴포넌트가 생각보다 자주 리렌더링되거나, 자식 컴포넌트가 의미 없이 반복해서 렌더링되는 문제를 경험하게 됩니다. 이럴 때 자주 등장하는 해결책이 바로 useCallback 훅입니다.

useCallback 은 함수의 참조값을 유지해 불필요한 렌더링을 줄이는 데 도움을 주는 도구이지만, 언제 써야 효과가 있고, 언제 쓰지 말아야 하는지에 대한 명확한 기준 없이 무작정 사용하는 경우가 많습니다.

이 글에서는 useCallback 기본 개념부터, 실제 사용 예제, 그리고 사용을 권장하는 상황과 피해야 할 상황까지 모두 정리합니다. 성능 최적화를 위해 useCallback 을 고민하고 있다면, 이 글이 그 판단에 확실한 기준을 줄 수 있을 거예요.

 


1.  useCallback

React에서 제공하는 훅으로, 특정 콜백 함수의 메모이제이션(memoization)을 도와줍니다. 쉽게 말해, 컴포넌트가 리렌더링될 때마다 매번 새로운 함수를 생성하지 않고, 의존성 배열(dependency array)에 명시된 값이 바뀌지 않는 한 동일한 함수 객체를 재사용할 수 있도록 해줍니다.

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

 


2.  주요 개념 요약

항목 설명
목적 불필요한 함수 재생성 방지
사용 위치 주로 자식 컴포넌트에 props 로 함수를 전달할 때
의존성 배열 의존성이 변경될 때만 새로운 함수 생성

 


3.  왜 useCallback이 필요한가요?

React는 컴포넌트가 리렌더링될 때, 함수도 새로 정의되기 때문에 참조값이 매번 바뀝니다. 이로 인해 하위 컴포넌트가 React.memo 로 최적화되어 있더라도, props 로 전달되는 함수가 매번 다르게 인식되어 불필요한 리렌더링이 발생할 수 있습니다.

예시 - useCallback 없이 발생하는 문제

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

  return <Child onClick={handleClick} />;
};

const Child = React.memo(({ onClick }) => {
  console.log('Child render');
  return <button onClick={onClick}>Click me</button>;
});

Parent 가 리렌더링되면 handleClick 함수는 새로운 참조값으로 재생성되므로, Child 는 항상 리렌더링됩니다.

useCallback을 사용한 최적화

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

  return <Child onClick={handleClick} />;
};
  • 이제 Parent 가 리렌더링되더라도 handleClick 함수는 의존성이 바뀌지 않는 한 같은 참조값을 유지하므로, Child 는 불필요하게 리렌더링되지 않습니다.

 


4.  언제 사용해야 할까?

a.  사용 권장 상황

i.  React.memo와 함께 자식 컴포넌트에 함수를 props로 전달하는 경우

const Child = React.memo(({ onClick }) => {
  console.log('Child render');
  return <button onClick={onClick}>Click</button>;
});
  • 부모 컴포넌트가 리렌더링될 때마다 함수가 새로 만들어지면, 자식 컴포넌트도 계속 리렌더링됩니다.
  • 이런 경우 useCallback 을 사용해 참조값을 유지해주면, 자식 컴포넌트의 불필요한 렌더링을 방지할 수 있습니다.
const handleClick = useCallback(() => {
  console.log('clicked');
}, []);

 

ii.  함수를 useEffect, useMemo, useReducer 등에 의존성으로 넣을 때

useEffect(() => {
  doSomething();
}, [callback]); // callback이 매번 바뀌면 이 useEffect도 계속 실행됨
  • React 훅에서는 함수도 의존성 배열에 넣어야 할 때가 있습니다.  하지만 함수가 매 렌더링마다 새로 만들어지면 useEffect 등이 의도치 않게 자주 실행될 수 있습니다.
  • 이런 경우 useCallback 을 사용해 함수가 불필요하게 바뀌지 않도록 할 수 있습니다.

b.  사용을 피해야 할 상황

i.  함수가 매우 가볍고, 자주 변경되어야 하는 경우

const handleChange = (e) => setValue(e.target.value);
  • 이런 함수는 그냥 다시 만들어도 전혀 부담이 없습니다. 오히려 useCallback으로 감싸면 메모이제이션에 오히려 비용이 들 수 있습니다.
  • 즉, 최적화하려는 비용보다 메모이제이션 유지 비용이 더 클 수 있습니다.

ii.  의존성이 자주 바뀌는 함수

const handleClick = useCallback(() => {
  console.log(count);
}, [count]);
  • count 가 자주 바뀐다면 handleClick 도 매번 새로 만들어지기 때문에 사실상 useCallback 효과가 거의 없습니다.
  • 이럴 때는 그냥 일반 함수로 두거나, 정말 성능 이슈가 있다면 로직 자체를 리팩토링해서 무거운 부분만 따로 분리하는 게 좋습니다.

c.  주의사항

  • 의존성 배열을 정확히 설정해야 합니다. 누락되면 stale value(오래된 값)를 참조할 수 있습니다.
  • 무조건적으로 useCallback 을 남발하면 성능이 개선되기보다는 오히려 나빠질 수도 있습니다. (메모이제이션 비용 자체가 존재하기 때문)
  • useCallback(fn, []) 은 실제로는 컴포넌트 최초 마운트 시 단 한 번만 생성되므로, 내부에서 상태를 참조하는 경우 주의가 필요합니다.

 


5.  마무리

  • useCallback 은 함수의 참조값을 유지하여 불필요한 리렌더링을 막아주는 훅입니다.
  • 주로 React.memo 와 함께 자식 컴포넌트를 최적화할 때 사용합니다.
  • 모든 함수에 일괄적으로 사용하는 것이 아니라, 최적화가 필요한 곳에 선별적으로 사용하는 것이 중요합니다.

함께 보면 좋은 자료

블로그 글 :

 

[React Hooks] useState부터 커스텀 훅까지

React Hooks React 16.8부터 등장한 Hooks(훅)은 함수형 컴포넌트에서도 상태 관리와 사이드 이펙트를 다룰 수 있게 해주며, 컴포넌트 구조를 더욱 직관적으로 바꿨습니다. 이 글에서는 자주 사용되는

dachaes-devlogs.tistory.com