본문 바로가기

React/실험실

[React] useMemo를 사용하지 말아야 한다!

React 애플리케이션에서 과도한 최적화는 최악의 상황을 나타낸다. 

몇몇의 개발자들은 useMemo와 useCallback을 개발에서 기본 사항으로 넣어서 모든 곳에 사용한다. 

하지만 useMemo는 오히려 애플리케이션의 속도를 저하시킬 수 있다. 

 

명심해야할 것은 메모이제이션은 공짜가 아니다! 

 

우리는 왜 useMemo를 사용할까? 

useMemo는 컴포넌트의 리렌더링 사이에서 계산 결과를 캐시에 저장하는 Hook이다. 

React.memo, useCallback, 디바운싱, 동시 렌더링(Concurrent Rendering) 등과 함께 사용해서 

성능상 이점을 준다.

 

이것이 특정 상황에선 정말 도움이 되고 중요한 역할을 하는 것은 맞지만, 대부분은 

부적절한 방법인, 모든 변수를 useMemo로 래핑한다. 

 

당연히 이런 방식은 오히려 가독성을 떨어뜨리고 메모리 사용량을 증가시킬 뿐이다. 

 

또한 useMemo는 리렌더링 단계에서만 이점이 있으며, 초기화 하는 동안 메모이제이션은 애플리케이션의 

속도를 저하시키며, 이 효과는 누적된다. 

 

이것이 메모이제이션이 공짜가 아니라는 이유이다. 

 

그렇다면 언제 유용하지 않거나 해로울까? 

export const NavTabs = ({ tabs, className, withExpander }) => {
  const currentMainPath = useMemo(() => {
    return pathname.split("/")[1];
  }, [pathname]);
  const isCurrentMainPath = useMemo(() => {
    return currentMainPath === pathname.substr(1);
  }, [pathname, currentMainPath]);

  return (
    <StyledWrapper>
      <Span fontSize={18}>
        {isCurrentMainPath ? (
          t(currentMainPath)
        ) : (
          <StyledLink to={`/${currentMainPath}`}>
            {t(currentMainPath)}
          </StyledLink>
        )}
      </Span>
    </StyledWrapper>
  );
};

위 코드 예제가 있다. 

여기서 useMemo는 어떠한 이점이 있을까? 

 

일반적으로 useMemo는 참조를 기억했다가 메모된 컴포넌트에 전달하거나, 비용이 많이 드는 계산을 캐시하는 

두가지 경우에 사용된다. 

 

이제 위 예시에서 무엇을 최적화하고 있는지 생각해보자. 

원시 값을 가지고 있고, 컴포넌트 트리에 더 깊은 값을 전달하지 않으므로 참조를 보존할 필요가 없다. 

또한 .split과 === 연산을 특별하게 어려운 계산이 아니다. 

 

따라서 예시에서 사용하고 있는 useMemo는 굳이 사용할 필요가 없으며, 제거하고 파일 공간을 절약할 수 있다. 

export const Client = ({ clientId, ...otherProps }) => {
  const tabs = useMemo(
    () => [
      {
        label: t("client withdrawals"),
        path: `/clients/${clientId}/withdrawals`
      },
      ...
    ],
    [t, clientId]
  );
  
  ...
  
  return (
    <>
      ...
      <NavTabs tabs={tabs} />
    </>
  )
}

export const NavTabs = ({ tabs, className, withExpander }) => {
  return (
    <Wrapper className={className} withExpander={withExpander}>
      {tabs.map((tab) => (
        <Item
          key={tab.path}
          to={tab.path}
          withExpander={withExpander}
        >
          <StyledLabel>{tab.label}</StyledLabel>
        </Item>
      ))}
    </Wrapper>
  );
};

 위 예시를 보자. 

tabs 변수를 메모하고, NavTabs에 전달한다. 

 

여기서의 useMemo, 최적화의 의도가 무엇일까?

이는 계산이 아니므로 참조를 보존하고 NavTabs이 과도하게 리렌더링하지 않기를 원했을 것이다. 이러한 방법이 

옳은 방법일까? 

 

우선 첫번째로, NavTabs는 성능에 영향을 주지 않고 여러 번 렌더링할 수 있는 가벼운 컴포넌트이다. 

둘째, 무거운 컴포넌트라고 useMemo를 통한 이점을 얻을 수 없다. 

클라이언트가 매번 NavTabs를 다시 렌더링하는 것을 막기 위해서는 참조를 유지하는 것으로는 충분하지 않다. 

여기서 성능을 최적화하려면 추가적으로 NavTabs를 React.memo로 감싸는 것이 중요하다. 

 

계산 비용이 비싼지 어떻게 알 수 있을까? 

수천 개의 항목에 대해 복잡한 루프를 수행하거나 팩토리얼 계산을 수행하지 않는 한 비용이 많이 들지 않을 수 있다. 

 

다음과 같은 경우에는 useMemo의 사용을 피해야 한다. 

최적화하려는 계산의 비용이 크지 않은 경우, 이러한 경우 useMemo를 사용할 때 발생하는 오버헤드가 이점보다
     클 수 있다. ( React 공식 홈페이지에서는 1ms 이상 걸리는 경우 메모 해두는 것이 좋다고 이야기 한다. )

메모이제이션이 필요한지 확실하지 않은 경우, useMemo 없이 작업한 다음, 문제가 발생하면 코드에 점진적으로 

     최적화를 적용하는 것이 좋다. 

메모하고 있는 값이 컴포넌트로 전달되지 않는 경우, 값이 JSX에서만 상요되고 컴포넌트 트리에 깊이 전달되지 않으면 

    대부분의 경우 최적화를 피할 수 있다. 

    다른 컴포넌트의 렌더링에 영향을 주지 않으므로 참조를 기억할 필요가 없다. 

의존성 배열이 너무 자주 변경되는 경우, 이 경운 useMemo는 항상 재계산되므로 성능적인 이점을 제공하지 않는다. 

 

출처 

https://javascript.plainenglish.io/stop-using-usememo-now-e5d07d2bbf70#5aca

 

Stop Using useMemo Now!

Most of the Time It Slows Down Your Application

javascript.plainenglish.io

 

반응형

'React > 실험실' 카테고리의 다른 글

[React] Cookie  (1) 2023.06.01
[React] Polymorphic 한 컴포넌트  (1) 2023.05.17
[React] React18에서 추가된 Hook  (0) 2023.04.17
[React] useState vs useReducer  (0) 2023.04.16
[React] CRA Path Alias 설정하기  (0) 2023.04.11