특정 함수를 새로 만들지 않고 재사용하고 싶은 경우 사용할 수 있는 함수 메모이제이션(memoization)용 리액트 훅이다. 이를 통해서 불필요한 함수 생성을 줄이고 성능을 최적화 할 수 있게 해준다.
useMemo와의 간단한 차이점이라고 한다면, useMemo는 값을 저장했던 반면 useCallback은 함수를 반환한다는 특징이 있다.
함수 컴포넌트 내에서 함수를 선언할 때마다 매번 새로운 함수 인스턴스가 생성될 수 있다. 이런 경우 자식 컴포넌트에게 props를 통해 전달되거나, useEffect내에서 의존성으로 사용된다면, 매 렌더링마다 새로운 함수가 생성되어 자식 컴포넌트의 불필요한 리렌더링을 유발할 수 있다.
useCallback은 이러한 상황에서 함수를 캐싱하여 같은 함수 인스턴스를 사용하도록 도와주며, 렌더링 성능을 최적화하는데 도움을 준다.
function ExampleComponent() {
const [count, setCount] = useState(0);
// 일반적인 함수 선언
const handleClick = () => {
setCount(count + 1);
};
// useCallback을 사용하여 함수 메모이제이션
const memoizedHandleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // count가 변경될 때만 함수가 새로 생성됨
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment (Without useCallback)</button>
<button onClick={memoizedHandleClick}>Increment (With useCallback)</button>
</div>
);
}
위 예저에는 두가지의 버튼이 존재한다. 첫번째 버튼(handleClick)은 매 렌더링마다 새로운 함수가 생성되기 때문에, 상태 업데이트 시에는 불필요한 자식 컴포넌트의 리렌더링이 발생 할 수 있다.
두번째 버튼(memoizedHandleClick)은 useCallback을 사용하여 상태가 변경될 때에만 새로운 함수가 생성되므로, 최적화된 함수 재사용이 가능해진다.
이론상 모든 함수를 useCallback으로 감싸주면 좋을 것 같은데, 남용의 문제점이 뭐가 있을까?
뭐든 과도한 남용은 문제가 되지만 useCallback만큼은 문제가 없다고 생각할 수 있다. 하지만 실제 성능과도 직결되는 문제점들이 몇몇가지 존재한다.
모든 함수에 useCallback을 적용하면 코드가 복잡해질 수 있다. 모든 함수를 메모이제이션하려고 하는 시도는 실제로 필요하지 않은 최적화로 이어질 수 있다.
useCallback을 모든 함수에 사용하게 되면, 모든 함수 인스턴스를 메모리에 유지해야 하므로 컴포넌트가 계속 렌더링 될 때마다 메모리 사용량이 증가할 수 밖에 없다.
의존성 배열에 상태나 props를 제대로 관리하지 않으면 오히려 함수의 불필요한 재생성으로 인해 렌더링 성능이 저하될 수 있다.
남용하게 되면 코드에 불필요한 복잡성이 추가될 수 있다. 함수가 자주 변경되지 않거나, 혹은 아예 변경되지 않는 경우, 해당 함수가 렌더링 성능에 큰 영향을 미치지 않는 경우에는 useCallback을 사용할 필요가 없다.
useCallback으로 인해 함수가 메모이제이션되면 디버깅 시 함수 이름이나 정보가 변하지 않아 디버깅이 어려울 수 있다.
모든 함수를 메모이제이션할 필요는 없다. 성능 개선이 명확하게 확인되는 경우에만 사용하는 것이 좋다.
useCallback을 사용할 때는 해당 컴포넌트의 특성과 성능의 특징을 고려하여 적절하게 사용하므로써 코드의 가독성과 유지보수성을 유지하면서 최대의 성능을 향상 시킬 수 있겠다.