본문 바로가기
🌳Frontend/react

Stop Using useMemo Now

by Bㅐ추 2023. 4. 18.
728x90
반응형

원문: https://javascript.plainenglish.io/stop-using-usememo-now-e5d07d2bbf70

번역: https://velog.io/@lky5697/stop-using-usememo-now

을 읽고 재정리하였습니다.


useMemo 란

useMemo 는 알다시피 계산결과를 memorization (유사하게, 기억한다 라고 생각하자.) 한다.

const result = useMemo(() => {
 
 // running....
 
 return value
}, [deps])

코드가 위와 같을 때, deps 로 들어온 state 값에 따라 side effect 로 useMemo 안의 로직이 실행되고 그 결과값을 리턴하게 된다.

deps 의 값에 따라 리턴되는 value 값이 다를텐데, 이때 useMemo 는 어떤 deps 일 때 value를 리턴하는 지 memorization 한다.

 

즉, 예를들어 아래처럼 순차적으로 실행되었다고 생각해보자.

 

1. deps => 'abc' 👉 계산중 ... 👉 value => 2

2. deps => ab' 👉 계산중 ... 👉 value => 3

 

그 다음 deps 가 다시 'abc' 가 된다면, 이미 1번에서 수행한 이력이 있기 때문에, 굳이 계산결과를 거치지 않고 캐싱한 2 를 리턴하는 것이다.

 

우린 어떨 때 useMemo 를 사용했을까 ?

계산 결과를 기억한다 라는 사실만 보면, 정말 은혜로운 hook 으로 보인다. 리엑트로 개발하다보면 한번쯤 side effect 에 따른 리렌더링에 대한 고민을 했을 것이다. 정말 깊게 생각하지 않는다면, (혹은 useMemo 의 사용법을 자세히 알지 못한다면 ) 단순히 계산결과를 기억한다는 것에 꽂혀 최적화를 한다는 명목하에 여기저기 useMemo 를 남발하게 된다.

 

하지만, 이것은 오히려 메모리 사용량을 증가하게 하는 최악의 상황을 불러오게 된다.

참고로, useMemo 는 렌더링 도중 실행된다.

 

useMemo 가 최악의 성능을 보이는 경우는 언제인가 ?

// props 로 tabs, className, withExpander 받아온다.
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>
  );
};

위의 예시 코드가 있다. 우선 하나하나 쪼개서 기능을 이해해보자.

 

  const currentMainPath = useMemo(() => {
    return pathname.split("/")[1];
  }, [pathname]);

 

props 로 받아온 pathname 을 '/' 로 split 하여 두번째 값 을 리턴한다.

 

  const isCurrentMainPath = useMemo(() => {
    return currentMainPath === pathname.substr(1);
  }, [pathname, currentMainPath]);

위에서 계산한 currentMainPath 와 pathname를 substr(1) 한 값이 같은지를 체크한다.

 

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

그리고 그 값에 따라 링크를 보여주는 것 같다.

 

지금 우리는 useMemo 을 두번 쓰고 있다. 이 두개의 로직에서 우리가 최적화하고 싶은건 무엇인지 생각해보자.

자세히 보면, split 와 === 와 같은 단순한 연산인데, useMemo 로 캐싱해야하는 건지도 불분명하다.

 

useMemo 를 걷어내보면 아래처럼 깔끔해지며 가독성을 챙길 수 있다.

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 사용은 피하자

  • 최적화하려는 계산의 비용이 크지 않은 경우. 
    • 이러한 경우, useMemo를 사용할 때 발생하는 오버헤드가 이점보다 클 수 있다.
  • 메모이제이션이 필요한지 확실하지 않은 경우. 
    • useMemo 없이 시작한 다음, 문제가 발생하면 코드에 점진적으로 최적화를 적용해보자.
  • 메모하고 있는 값이 컴포넌트로 전달되지 않는 경우. 
    • 이 값이 JSX에서만 사용되고 컴포넌트 트리에 더 깊이 전달되지 않으면 대부분의 경우 최적화를 피할 수 있다. 다른 컴포넌트의 렌더링에 영향을 주지 않으므로 참조를 기억할 필요가 없다.
  • 의존성 배열이 너무 자주 변경되는 경우. 
    • 이 경우 useMemo는 항상 재계산되므로 성능적인 이점을 제공하지 않는다.
728x90
반응형