React

리액트 성능 최적화 : useCallback, useMemo, Memo

liinyeye 2024. 12. 30. 22:38

프로젝트를 하다보면 성능 최적화를 의식해 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의 비용이 렌더링 비용보다 클 수 있음.

 

 


참고 자료

  • 리액트 공식 문서