프로젝트를 하다보면 성능 최적화를 의식해 useCallback, useMemo와 Memo를 습관적으로 사용하지만 정작 어떤 차이가 있는지 정확히 알지 못한 채 사용하고 있다는 것을 깨달았다. 그래서 useCallback, useMemo와 Memo 각각의 사용법에 대해 개념부터 다시 제대로 알아보려고 한다.
useCallback
함수의 재생성을 방지하는 React Hook
- 컴포넌트가 리렌더링되더라도 동일한 의존성 배열을 갖는 한, 이전에 생성한 함수를 재사용
- 함수가 자식 컴포넌트에 props로 전달될 때 유용. 왜냐면, 새로 생성된 함수는 참조가 바뀌어 자식 컴포넌트가 불필요하게 리렌더링될 수 있기 때문
어떻게 사용하는지?
컴포넌트의 리랜더링 방지
자바스크립트에서 function (){} 나 () => {} 은 항상 다른 함수를 생성한다. 따라서, 여기서 handleSubmit함수를 useCallbakc으로 감싸주지 않는다면 ProductPage 컴포넌트가 리렌더링 될때마다 새로운 함수가 생기게 되어 ShippingForm의 props는 절대 같아질 수 없고 자식컴포넌트의 memo최적화는 동작하지 않게 될 것이다.
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
// useCallback으로 감싸지 않는다면 theme이 바뀔때마다 다른 함수가 될 것입니다...
// 따라서, useCallback으로 감싸 React에게 리렌더링 간에 함수를 캐싱하도록 요청합니다...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...이 의존성이 변경되지 않는 한...
return (
<div className={theme}>
{/* ... 그래서 useCallback으로 감싸지 않을 경우, ShippingForm의 props는 같은 값이 아니므로 매번 리렌더링 할 것입니다. */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
커스텀훅 최적화하기
커스텀 훅을 작성하는 경우, 반환하는 모든 함수를 useCallback으로 감싸는 것이 좋다. 이렇게 하면 Hook을 사용하는 컴포넌트가 필요할 때 가지고 있는 코드를 최적화할 수 있다.
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,
};
}
useMemo
리랜더링 사이에 계산 결과를 캐싱할 수 있게 해주는 React Hook
- 비용이 높은 로직의 재계산 생략
- 초기 렌더링 시, useMemo에서 얻을 수 있는 값은 계산 함수를 호출한 결과값이며, 종속성이 변경되기 전까지 리렌더링 사이의 계산 결과를 캐싱함
useCallback과 useMemo는 어떤 연관이 있나요?
두 훅은 모두 자식 컴포넌트를 최적화할 때 유용하다. 무언가를 전달할 때 memoization(캐싱)을 할 수 있도록 해준다.
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // 함수를 호출하고 그 결과를 캐싱합니다.
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // 함수 자체를 캐싱합니다.
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
차이점은 무엇을 캐싱하는지!
- useMemo는 호출한 함수의 결과값을 캐싱. 위 예시에서는 computeRequirements(product) 함수 호출 결과를 캐싱해서 product가 변경되지 않는 한 이 결과값이 변경되지 않도록 함. 이것은 불필요하게 ShippingForm을 리렌더링하지 않고 requirements객체를 넘겨줄 수 있도록 해주며, 필요할 때 React는 렌더링 중에 넘겨주었던 함수를 호출하여 결과를 계산함.
- useCallback은 함수 자체를 캐싱. 전달할 함수를 캐싱해서 productId나 referrer이 변하지 않으면 handleSubmit자체가 변하지 않도록 함. 이것은 불필요하게 ShippingForm을 리렌더링하지 않고 handleSubmit함수를 전달할 수 있도록 해줌.
React.memo
React.memo는 props가 변경되지 않았을 때 렌더링을 막아주는 최적화 도구
부모 컴포넌트가 렌더링될 때 자식 컴포넌트도 리렌더링되는 상황에서, props가 변하지 않는다면 불필요한 렌더링을 방지. 하지만 props 변경 빈도 확인했을 때 isSelected와 같은 props의 변경 빈도가 높다면, React.memo의 비용이 렌더링 비용보다 클 수 있음.
참고 자료
- 리액트 공식 문서