(번역) useMemo를 사용하는 것을 당장 멈추세요!

강엽이·2023년 4월 13일
132
post-thumbnail

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

부제: 대부분의 경우 애플리케이션의 속도가 저하됩니다.

리액트 애플리케이션에서의 과도한 최적화는 실제로 악몽과도 같습니다. 우리는 매일 코드를 더 "미스터리"하게 만드는 수많은 쓸모없는 최적화에 직면합니다. 일부 개발자들은 단순화하기 위해 useMemo와 useCallback을 기본 스타일 가이드에 포함하기도 합니다. useMemo는 여러분들의 애플리케이션 속도를 저하시킬 수 있기 때문에 그런 착각에 빠지지 마세요! 기억하세요 메모이제이션은 대가 없이 제공되지 않습니다.

이 글에서는, 대부분의 개발자들이 어떻게 useMemo 훅을 남용하는지 보여주고 그것을 피하는 몇 가지 팁을 제공하고 싶습니다. 제가 이런 실수를 한다는 것을 처음 알았을 때, 어리석었다고 느꼈습니다. 긴 말 하지 않고, 시작해 보겠습니다!

목차

우리는 왜 useMemo를 사용할까요?

useMemo는 컴포넌트 리렌더링 간에 계산 결과를 캐싱할 수 있는 훅입니다. 성능상의 이유로만 사용되며, React.memo, useCallback, 디바운싱, 동시 렌더링 등과 같은 기술과 함께 사용해야 한다는 것은 모두가 알고 있습니다.

일부 상황에서는 이 훅이 실제로 도움이 되고 중요한 역할을 하지만 대부분의 개발자가 부적절하게 사용합니다. 그들은 무작위로 최적화가 성공하기를 바라며 모든 변수를 useMemo로 감쌉니다. 이 방법은 그저 가독성이 떨어지고 메모리 사용량이 증가할 뿐입니다.

똑똑한 엔지니어의 가장 일반적인 오류는 존재하면 안 되는 것을 최적화하는 것일 수 있다. — 일론 머스크

한 가지 더 기억해야 할 중요한 사항은 useMemo는 렌더링 단계에서만 값을 제공한다는 것입니다. 초기화하는 동안 메모이제이션은 애플리케이션의 속도를 늦추고 이 효과는 쌓이는 경향이 있습니다. 이것이 제가 말한 "메모이제이션은 대가 없이 제공되지 않습니다."라는 뜻입니다.

언제 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는 참조를 기억하여 메모된 컴포넌트에 추가로 전달하거나 값비싼 계산을 캐시하는 두 가지 경우에 사용됩니다.

지금 잠시 생각해보세요. 위의 예시에서 우리가 최적화하고 있는 것은 무엇일까요? 우리는 원시 값을 가지고 있고 컴포넌트 트리에 더 깊은 값을 전달하지 않으므로 참조를 보존할 필요가 없습니다. 또한, .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>
  );
};

위의 예시에서, 탭 변수를 메모한 다음 NavTabs에 전달합니다. 탭 생성 최적화의 주요 목적이 무엇이라고 생각하십니까? 이는 전혀 계산이 아니므로, 이를 구현한 사용자는 참조를 보존하고 NavTabs의 과도한 렌더링을 피하고 싶었을 것입니다. 이것을 여기서 하는 것이 옳은 것일까요?

우선, NavTabs은 성능에 영향을 주지 않고 여러 번 렌더링 할 수 있는 가벼운 컴포넌트입니다. 두번째로, 무거운 컴포넌트라고 하더라도 useMemo는 동작하지 않습니다. 클라이언트 컴포넌트가 렌더링 될 때마다 NavTabs의 리렌더링을 방지하기 위해서는 참조를 유지 하는 것만으로 충분하지 않습니다. 성능을 최적화하려면 NavTabs를 React.memo로 묶어야 합니다.

메모 함수는 컴포넌트의 메모된 버전을 반환합니다. 이 버전은 일반적으로 프로퍼티 값이 변경되지 않는 한 상위 버전이 변경될 때 다시 렌더링되지 않습니다. 메모에서는 컴포넌트를 업데이트해야 하는지 여부를 결정하기 위해 프로퍼티의 얕은 비교를 사용합니다. 컴포넌트를 다시 렌더링해야 하는 특정 조건이 있는 경우 두 번째 인수로 자체 비교 함수를 지정할 수도 있습니다.

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

수천 개의 항목에 대해 복잡한 루프를 수행하거나 팩토리얼 계산을 수행하지 않는 한 비용이 많이 들지 않을 수 있습니다. performance.now()와 결합된 리액트 프로파일러를 함께 사용하여 병목 현상을 파악한 다음 최적화 기법을 적용할 수 있습니다.

다음과 같은 경우에서는 useMemo의 사용을 피하세요.

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

올바르게 사용하는 방법에 대한 팁

리액트 컴포넌트는 리렌더링 할 때마다 본문을 실행하며, 이는 해당 컴포넌트의 상태 혹은 프로퍼티 값이 변경될 때 일어납니다. 일반적으로 렌더링 중에는 컴포넌트의 속도가 느려질 수 있으므로 비용이 많이 드는 계산은 피하고 싶을 겁니다.

하지만 대부분의 계산은 여전히 매우 빠르기 때문에 실제로 메모를 사용해야 하는 위치를 파악하기 어렵습니다. 효과적이고 의도적인 최적화 통합을 시작하려면 먼저 문제를 파악해야 합니다. 또한, useCallback, React.memo, 디바운싱, 코드 분리, 지연 로딩과 같은 다른 기술도 잊지 마세요.

아래의 정말 간단한 예를 보겠습니다. doCalculation 함수는 인위적으로 느려진 함수이므로 임의의 숫자를 반환하는데 500ms가 걸립니다. 그럼 우리는 여기서 어떤 문제를 가지게 될까요? 네, 값이 업데이트 될 때마다 무거운 함수를 다시 계산하기 때문에 input을 사용하는 것이 어렵습니다.

function doCalculation() {
  const now = performance.now();
  while (performance.now() - now < 500) {
    // 500ms동안 아무것도 하지 않음. 
  }

  return Math.random();
}

export default function App() {
  const [value, setValue] = useState(0);
  const calculation = doCalculation();

  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <h1>{`Calculation is ${calculation}`}</h1>
    </div>
  );
}

이 문제를 어떻게 해결할 수 있을까요? 종속성 없이 useMemo로 doCalculation을 감싸기만 하면 됩니다.

const calculation = useMemo(() => doCalculation(), []);

Live Example

https://0mjpbw.csb.app/

더 배우고 싶다면?

읽어주셔서 감사합니다.

여러분들에게 이 글이 유용하셨기를 바랍니다. 질문이나 제안이 있으시면 댓글로 남겨주세요. 여러분의 피드백은 제가 더 나아질 수 있도록 도와줍니다.

구독하는 것을 잊지마세요.⭐️

PlainEnglish.io.에 콘텐츠들이 더 있습니다.

저희의 무료 주간 뉴스레터에 가입하세요. 저희 Twitter, LinkedIn, YouTube 그리고 Discord를 팔로우 해주세요.

여러분의 소프트웨어 스타트업에서 확장에 관심이 있으신가요? Circuit을 확인하세요.

profile
FE Engineer

2개의 댓글

comment-user-thumbnail
2023년 4월 21일

앗 제가 찾고 있던 내용이라 정말 도움이 되네요 ㅎㅂㅎ 잘 보고 갑니다 감사합니다.

답글 달기
comment-user-thumbnail
2023년 5월 20일

Great Article . Very helpful. Thanks for sharing. salesforce certification course

답글 달기