관리 메뉴

꿈꾸는 개발자

useMemo를 사용하는 기준 - (언제 사용하나? 지양하는 게 더 좋다!?!) 본문

React.js

useMemo를 사용하는 기준 - (언제 사용하나? 지양하는 게 더 좋다!?!)

rickysin 2023. 9. 17. 16:01

원티드 프론트엔드 최적화 강의를 들었을 때 현업에서는 useMemo, useCallback을 자주 사용하지 않는다는 말을 들었다. 

 

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

위 링크는 useMemo를 난발하면 안 좋은 점과 최적화가 아닌, 오히려 App의 성능을 저하할 수도 있다고 말한다.  컴퓨터공학은 항상 trade-off를 고려해야 한다. 이 말은 즉슨 최적화에 따른 댓가가 분명 있다는 것! 

 

일차적으로 useMemo, 그 자체보단 React.memo, useCallback debouncing concurrent rendering 등과 합쳐져야 한다. 저자에 따르면, useMemo 등의 최적화 과정은 분명히 도움이 되는 곳이 있지만, 모든 value에 useMemo를 사용 후 적절한 최적화가 되기를 바라는 것은 적합하지 않다고 말한다. 이는 오히려 메모리 사용량을 증가시킬 뿐이다! 

 

useMemo는 re-render phase에서 value를 가져온다. 따라서, 초기화 및 메모제이션 과정에서 app 자체의 속도를 저하시킨다. 


보통 useMemo를 사용하는 경우는

  • 복잡한 계산이 들어가는 경우 
  • Memoized component을 전달하는 경우
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를 제거해도 무방하다는 것이다.

 

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>
  );
};

 위 코드의 경우 useMemo보단, React.memo를 사용해서 부모 컴포넌트가 re-rendering되더라도, props의 변화가 없는 한 재랜더링되지 않도록 코드를 작성해야 적절한 optimization이 된다. (React.memo의 경우 shallow compraison을 시행하지만, 두 번째 arg로 사용자가 원하는 검사 조건을 추가할 수 있다) 


계산이 복잡하다는 것의 기준은?

thousands의 items의 계산, 팩토리얼 계산이 아닌 이상 대체적으로 복잡한 계산에 속하지 않는다! Profiler을 활용해 optimization이 필요한 컴포넌트를 찾을 수 있다. 

 

https://react.dev/learn/you-might-not-need-an-effect

 

You Might Not Need an Effect – React

The library for web and native user interfaces

react.dev

 

추가로 위 React 공식문서에 따르면, 

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

위 처럼 시간을 측정하는 함수를 통해 함수 실행 시간을 측정했을 때 1ms 이상일 경우에만 useMemo의 사용을 고려해도 좋다고 말한다. useMemo를 사용한 후 동일하게 실행 시간이 유의미하게 감소했는지 확인 해보는 것도 좋다. 

 

기억해야 할 것은: useMemo는 첫 번째 render를 빠르게 해주는 게 아니라, memoization을 re-render을 빠르게 해주는 것이기 때문에 잘 판단하고 사용하는 게 중요하다. 


이러한 경우에는 useMemeo를 사용하지 마라!

  • 가벼운 계산일 경우 
  • memoization이 필요한지 애매한 경우: 처음에는 제외하고 나중에 문제가 생기면 추가하는 것이 좋다.
  • memoizing하고 있는 값이 component에 전달되지 않는 경우
  • dependency array가 너무 자주 변동되는 경우: 

적확하게 사용하는 꿀팁!

React component는 props나 state가 change될 때마다  re-rendering이 발생한다. 따라서, rendering 단계에서 최적화를 하기 위해서는 문제에 대한 근본적인 판단이 우선이 되어야 한다. 나아가, React.memo, debouncing, code-splitting, lazy-loading 등을 항상 고려하는 것도 중요하다.