[React] useMemo

IKNOW·2024년 5월 2일

useMemo는 계산 결과를 캐싱해주는 React Hook이다.

	const cachedValue= useMemo(cacluateValue, dependencies) 
  • calculateValue: 캐싱하려는 값을 계산하는 함수. 순수 해야한다.
  • dependencies: 위의 calculateValue에서 참조된 모든 값들의 목록. React는 Object.is()의존성들을 이전 값과 비교한다.

useMemo는 처음에는 calculateValue를 호출한 결과를 반환하자만, 다음 렌더링에서는 저장된 값을 반환하거나 calculateValue를 다시 호출하고 반환된 값을 저장한다.

비용이 높은 로직의 재 계산 생략

import { useMemo } from 'react';
function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(()=>filterTodos(todos, tab), [todos, tab]);
}

//todos, tab이 변경된다면
function TodoList({ todos, tab, theme }) {
  const visibleTodos = filterTodos(todos, tab), [todos, tab]
}

대부분의 계산은 매우 빠르기때문에 useMemo가 필요하지 않지만, 매우 큰 배열을 필터링하거나 비용이 많이 드는 계산을 수행하는 경우 사용하는 것이 좋다.

또한 성능 최적화를 위해서만 useMemo를 사용해야 하고, 먼저 기능을 구현하고 useMemo를 사용하여 성능을 개선해야한다.

비싼 연산인지 확인하는 방법

console.time('filter array')
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array')

위와같이 소요되는 시간을 측정할 수 있고, chrome의 CPU 스로틀링 옵션을 사용하여 인위적으로 낮능 성능으로 테스트할 수도 있다.

유용한 경우

  • useMemo 에 사용된 계산이 매우 느리거나, 종속성이 거의 변경되지 않는 경우.
  • memo로 감싸진 컴포넌트에 prop으로전달하는 경우 자식 컴포넌트의 렌더링을 스킵할 수 있다.
  • 전달한 값을 나중에 일부 Hook의 종속성으로 이용하는 경우.

이외의 경우에는 useMemo로 감싸는 것에 대한 이득이 매우 적을 수 있다.

컴포넌트 재렌더링 스킵

React는 컴포넌트가 다시 렌더링될때, 모든 자식 컴포넌트를 재귀적으로 다시 렌더링한다. 그렇기 때문에 자식컴포넌트의 렌더링이 느리다면 자식컴포넌트를 memo 로 감싸서 props가 마지막 렌더링 시점과 동일한 경우 다시 렌더링 하는 것을 생략할 수 있다.

사실 컴포넌트를 memo로 감싸는 것은 useMemo로 감싸도 된다. 다만 편리한 방법이 아니기 때문에 memo로 감싸는 것을 추천.

다른 Hook의 종속성 메모화

function Dropdown({ allItems, text}) {
	const searchOptions = { matchMode: 'whole-word', text };
	const visibleItems = useMemo(()=> {
		searchItems(allItems, searchOptions)
	}, [allItems, searchOptions])
}

위 코드와 같이 어떤 객체에 의존하게 되면 컴포넌트가 렌더링 될 때마다 모든 코드가 다시 실행되기 때문에(searchOptions가 매번 바뀐다) 메모이제이션의 효과가 나타나지 않는다.

이 때는 searchOptions또한 메모이제이션 하면

function Dropdown({ allItems, text}) {
	const searchOptions = useMemo(()=>{ matchMode: 'whole-word', text }, [text]);
	const visibleItems = useMemo(()=> {
		searchItems(allItems, searchOptions)
	}, [allItems, searchOptions])
}

text가 변하지 않는다면 → searchOptions가 변하지 않는다 → visibleItems가 변하지 않는다.

이 구조를 조금 개선한다면

function Dropdown({ allItems, text}) {
	const visibleItems = useMemo(()=> {
		const searchOptions = { matchMode: 'whole-word', text };
		return searchItems(allItems, searchOptions)
		}, [allItems, text])
}

함수 메모화

함수를 만드는 것은 문제가 아니고 피해야하는 일은 아니다. 다만 정의된 함수는 자식 컴포넌트를 렌더링하게 만들 여지가 있다.

export default function ProductPage({ productId, referrer }) {
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }

  return <Form onSubmit={handleSubmit} />;
}

위의 ProductPage 컴포넌트가 렌더링 되면서, handleSubmit은 매번 새로운 함수가 되기 때문에 Form 컴포넌트 또한 매번 렌더링 되게 된다.

export default function Page({ productId, referrer }) {
  const handleSubmit = useMemo(() => {
    return (orderDetails) => {
      post('/product/' + productId + '/buy', {
        referrer,
        orderDetails
      });
    };
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}

useMemo를 사용하여 새로운 함수를 메모이제이션하여 사용할 수 있는데, 이때 이렇게 사용하는 것보다 새로운 Hook인 useCallback을 사용할 수 있다.

export default function Page({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}

다음번에는 이어서 useCallback을 정리해보겠다!

profile
조금씩,하지만,자주

0개의 댓글