useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook입니다.
const cachedFn = useCallback(fn, dependencies)
기본 규칙
리렌더링 간에 함수 정의를 캐싱하려면 컴포넌트의 최상단에서 useCallback을 호출하세요.
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
매개 변수
1. 캐싱할 함수 : 캐싱을 진행할 콜백 함수를 전달합니다. React는 첫 렌더링에서 이 함수를 반환합니다. 이후 렌더링에서 dependencies 값이 이전과 같다면 React는 같은 함수를 다시 반환합니다. 반대로 dependencies 값이 변경되었다면 이번 렌더링에서 전달한 함수를 반환하고 나중에 재사용할 수 있도록 이를 저장합니다.
2. dependencies, 의존성 배열 : 캐싱할 함수 내부에서 참조되는 모든 반응형 값의 목록입니다. 반응형 값은 props와 state, 그리고 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함합니다. lint가 React를 위한 설정으로 구성되어 있다면 모든 반응형 값이 의존성으로 올바르게 명시되어 있는지 검증합니다. React는 Object.is 비교 알고리즘을 이용해 이전 값과 비교합니다.
반환값
최초 렌더링에서는 useCallback은 전달한 함수를 그대로 반환합니다.
후속 렌더링에서는 이전 렌더링에서 이미 저장해 두었던 함수를 반환하거나, 현재 렌더링 중에 전달한 함수를 그대로 반환합니다.
용법
함수 캐싱이 유용한 상황을 알아보겠습니다. 일반적으로 컴포넌트 리렌더링 시에 발생하는 함수 재선언을 방지하고자 useCallback을 사용하는 것은 좋은 접근이 아닙니다. 왜냐하면 useCallback 자체도 비용이 드는 작업이기 때문입니다. 작은 함수에 대해 모두 useCallback을 적용한다면, React는 이러한 함수들의 캐싱을 유지하는 것이 함수를 재선언하는 것 보다 비용이 클 수 있습니다. 또한 무의미한 Hooks를 남발하는 것은 코드의 복잡성과 가독성을 떨어트릴 수 있습니다.
주로 useCallback은 부모 컴포넌트에서 Memoization된 컴포넌트에게 함수를 prop으로 전달할 때 유용하게 사용됩니다.
컴포넌트 메모이제이션
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
};
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
// ShippingForm.jsx
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
위의 코드를 예시로 보겠습니다. ProductPage는 자식 컴포넌트로 ShippingForm을 가지고 있으며, ShippingForm 컴포넌트는 memo되어 있습니다.
기본적으로, 컴포넌트가 리렌더링할 때 React는 이것의 모든 자식을 재귀적으로 재렌더링합니다. 따라서 ProductPage에서 state가 변경됨에 따라 리렌더링이 발생하게 되면, ShippingForm 또한 재렌더링을 하게 됩니다. 만약 ShippingForm 또한 많은 자식 컴포넌트를 가지고 있어 리렌더링에 많은 비용이 발생하게 된다면 성능적인 이슈가 발생할 수 있습니다. 이럴 때 React에서 제공하는 memo를 사용하여 컴포넌트를 감싸게 되면, 마지막 렌더링과 동일한 props일 때 리렌더링을 건너뛰도록 할 수 있습니다.
하지만 위의 코드의 ShippingForm은 ProductPage가 리렌더링될 때 memo를 사용했음에도 똑같이 리렌더링이 일어납니다.
ProductPage 내부에 handleSubmit은 해당 컴포넌트가 리렌더링될 때 마다 함수를 재선언하기 때문에, ShippingForm의 props는 절대 같아질 수 없는 것입니다. 이는 ()=>{}와 같은 함수 리터럴 뿐만 아니라 function 키워드를 사용한 함수 선언 또한 똑같습니다.
이럴 때 useCallback으로 해당 함수를 감싸주면, 의존성이 변경되기 전까지는 같은 함수라는 것을 보장합니다.
function ProductPage({ productId, referrer, theme }) {
// React에게 리렌더링 간에 함수를 캐싱하도록 요청합니다...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...이 의존성이 변경되지 않는 한...
return (
<div className={theme}>
{/* ...ShippingForm은 같은 props를 받게 되고 리렌더링을 건너뛸 수 있습니다.*/}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
결과적으로 ProductPage가 리렌더링 되어도, 캐싱된 함수를 재생성하지 않기 때문에 ShippingForm의 props가 변하지 않게되어 불필요한 리렌더링을 방지하게 됩니다.
useCallback은 성능 최적화를 위한 용도로만 사용해야 합니다. 해당 함수가 성능적인 최적화가 필요 없으면 사용하지 않으면 됩니다.
Effect가 너무 자주 실행되는 것을 방지하기
가끔 Effect 안에서 함수를 호출해야 할 수도 있습니다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 문제점: 이 의존성은 매 렌더링마다 변경됩니다.
이또한 마찬가지로 createOptions는 매 렌더링마다 재생성되어 변경되기 때문에 해당 Effect는 재렌더링마다 설정 함수를 실행하게 됩니다. 이를 해결하기 위해, Effect에서 호출되는 함수 createOptions를 useCallback으로 감쌀 수 있습니다.
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ roomId가 변경될 때만 변경됩니다.
이제 createOptions는 리렌더링 간에 roomId가 같다면 createOptions 함수는 같다는 것을 보장합니다.
하지만 위와 같은 경우는 함수의 의존성을 제거하는 것이 더 좋습니다. 간단하게 함수 선언을 Effect 안으로 이동시키세요.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ useCallback이나 함수 의존성이 필요하지 않습니다.
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]);
커스텀 Hook 최적화하기
커스텀 Hook을 작성하는 경우, 반환하는 모든 함수를 useCallback으로 감싸는 것이 좋습니다.
function useRouter() { // 커스텀 훅
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
이렇게 하면 Hook을 사용하는 컴포넌트가 필요할 때 가지고 있는 코드를 최적화할 수 있습니다.
해당 게시글은 리액트 공식 문서를 정리한 글입니다. 추가적인 내용은 아래의 링크를 참고해 주세요. :)
https://ko.react.dev/reference/react/useCallback
useCallback – React
The library for web and native user interfaces
ko.react.dev