React 개발을 하다 보면 useCallback이라는 훅을 심심치 않게 만나게 됩니다. "함수를 메모이제이션한다"는데, 이걸 왜 써야 하고 언제 써야 효과적일까요? 오늘은 useCallback의 필요성과 사용법을 쉽고 명확하게 알아보겠습니다.
함수가 새로 태어난다고? React 렌더링의 비밀
먼저 React 컴포넌트가 어떻게 동작하는지 떠올려 봅시다. 컴포넌트는 자신의 상태(state)나 부모로부터 받은 속성(props)이 변경되면 다시 렌더링됩니다. 이때 중요한 점은, 컴포넌트 함수 본문 전체가 다시 실행된다는 것입니다. 즉, 컴포넌트 내부에 정의된 함수들은 기본적으로 렌더링될 때마다 새로 만들어집니다.
function MyComponent() {
const [count, setCount] = useState(0);
// MyComponent가 리렌더링될 때마다 이 함수는 '새로운' 함수가 됩니다.
const handleClick = () => {
console.log("클릭!");
};
return (
<div>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
{/* 자식 컴포넌트에 함수를 props로 전달 */}
<MyButton onClick={handleClick} />
</div>
);
}
// React.memo로 최적화된 자식 컴포넌트
const MyButton = React.memo(({ onClick }) => {
console.log("자식 버튼 리렌더링!");
return <button onClick={onClick}>자식 버튼</button>;
});
위 코드에서 "카운트 증가" 버튼을 누르면 MyComponent가 리렌더링되고, handleClick 함수도 새로 만들어집니다.
그래서 뭐가 문제인데? 불필요한 리렌더링!
MyButton 컴포넌트는 React.memo로 감싸져 있습니다. React.memo는 props가 이전 렌더링과 정확히 같다면(참조 동일성) 리렌더링을 건너뛰는 똑똑한 기능이죠.
하지만 handleClick 함수는 렌더링마다 새로 만들어지기 때문에, MyButton 입장에서는 onClick prop이 계속 **다른 값(다른 함수의 참조)**으로 전달되는 셈입니다. JavaScript에서 함수는 객체이고, 새로 만들어진 함수는 이전 함수와 내용이 똑같아도 다른 객체니까요.
결국, MyButton은 실제로는 아무것도 변하지 않았는데도 부모(MyComponent)가 리렌더링될 때마다 불필요하게 같이 리렌더링됩니다. (콘솔에 "자식 버튼 리렌더링!"이 계속 찍히는 것을 확인해보세요.)
구세주 등장! useCallback
useCallback은 바로 이 문제를 해결하기 위해 탄생했습니다.
- useCallback(fn, deps)은 함수 fn 자체를 메모이제이션합니다.
- 의존성 배열 deps 안의 값이 변경되지 않는 한, 함수를 새로 만들지 않고 이전에 만들었던 함수의 참조를 그대로 반환합니다.
function MyComponent() {
const [count, setCount] = useState(0);
// useCallback으로 handleClick 함수를 메모이제이션
// 의존성 배열이 비어있으므로([]) 이 함수는 처음 마운트될 때만 생성되고,
// 이후에는 계속 같은 참조를 유지합니다.
const handleClick = useCallback(() => {
console.log("클릭!");
// 만약 함수 안에서 count 같은 상태를 사용한다면 deps에 넣어줘야 합니다!
// 예: console.log(count); -> useCallback(..., [count]);
}, []); // 의존성 배열
return (
<div>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
<MyButton onClick={handleClick} /> {/* 이제 동일한 참조가 전달됨 */}
</div>
);
}
// 자식 컴포넌트 (React.memo 사용)
const MyButton = React.memo(({ onClick }) => {
console.log("자식 버튼 리렌더링!");
return <button onClick={onClick}>자식 버튼</button>;
});
이제 "카운트 증가" 버튼을 눌러도 handleClick 함수의 참조는 동일하게 유지됩니다. React.memo는 onClick prop이 변경되지 않았다고 판단하고, MyButton은 더 이상 불필요하게 리렌더링되지 않습니다! (콘솔에 "자식 버튼 리렌더링!"이 찍히지 않아요!)
언제 useCallback을 써야 할까?
- React.memo로 최적화된 자식 컴포넌트에 함수를 props로 전달할 때: 자식 컴포넌트의 불필요한 리렌더링을 막아 성능을 개선할 수 있습니다. (가장 흔한 사용 사례!)
- 함수를 다른 Hook(useEffect, useMemo)의 의존성 배열에 포함시킬 때: 함수의 참조가 안정적이지 않으면 해당 Hook이 불필요하게 계속 실행될 수 있습니다. useCallback으로 함수 참조를 안정화시키면 이를 방지할 수 있습니다. (위의 시간 버그 해결 사례에서도 사용되었죠!)
주의! 남용은 금물
useCallback 자체도 함수를 비교하고 메모리에 저장하는 약간의 비용이 듭니다. 따라서 모든 함수를 useCallback으로 감쌀 필요는 없습니다. 위에서 설명한 것처럼 **참조 동일성이 꼭 필요한 경우(성능 최적화가 필요한 경우)**에 사용하는 것이 좋습니다.
마무리
useCallback은 React 애플리케이션의 성능을 최적화하는 데 유용한 도구입니다. 함수의 참조 동일성을 유지해야 하는 상황을 잘 파악하고 적재적소에 활용한다면, 더 부드럽고 효율적인 사용자 경험을 제공할 수 있을 것입니다.
'Framework > React' 카테고리의 다른 글
[React] React 시간 관련 무한 루프 버그 해결기 (0) | 2025.04.16 |
---|---|
[React] useState setState 타입 설정 (0) | 2025.04.03 |
이벤트 전달 방지 e.stopPropagation() (0) | 2025.02.12 |
[React] useMutation 콜백 옵션(isSuccess에서 variables 등등까지) (0) | 2025.02.04 |
[React] useRef 응용 - toggle기능 만들기 (1) | 2025.02.04 |