무지성 useMemo, 멈춰!

December 08, 2021

  • React
  • React Hooks

두 코드 중, 성능이 더 좋은 코드는?

const dispense = React.useCallback((candy) => {
  setCandies((allCandies) => allCandies.filter((c) => c !== candy));
}, []); 
const dispense = (candy) => {
  setCandies((allCandies) => allCandies.filter((c) => c !== candy));
}; 

정답은 useCallback을 쓰지 않은 후자

간단한 인라인 로직을 메모이제이션 하는것은 오히려 많은 비용이 들기 때문

useMemo의 비용 계산해보기

자세한 내용은 Should You Really Use useMemo in React? Let’s Find Out. 참조

시간 복잡도와 같은 좀 더 정확한 지표로 비교를 했으면 좋았겠지만, 말하고자 하는 바는 같아 이 자료를 인용했다

아래와 같은 코드를 가진 Normal 컴포넌트와 Memo 컴포넌트가 있다고 하자.

import React from 'react';
const BenchmarkNormal = ({level}) => {
    const complexObject = {
        values: []
    };
    for (let i = 0; i <= level; i++) {
        complexObject.values.push({ 'mytest' });
    }
    return ( <div>Benchmark level: {level}</div>);
};
export default BenchmarkNormal; 
    - import React, {useMemo} from 'react';
const BenchmarkMemo = ({level}) => {
    const complexObject = useMemo(() => {
        const result = {
            values: []
        };
        
        for (let i = 0; i <= level; i++) {
            result.values.push({'mytest'});
        };
        return result;
    }, [level]);
    return (<div>Benchmark with memo level: {level}</div>);
};
export default BenchmarkMemo; 

useMemo Benchmark

  • n=1일때 초기 렌더링은 19% 느려짐
  • n=100인 경우 초기 렌더링이 62%느려지나 리렌더링은 약간 빨라짐
  • 근데 이것도 굳이? memo 쓸 필요 있나? 싶어짐
  • useMemo를 사용한 초기 렌더링은 성능 면에서 상당한 비용을 지불함
  • 이 글의 작성자는 약 5-10%의 초기 성능 손실을 예상했지만, useMemo는 데이터/처리 복잡성에 크게 의존하고 500% 정도의 성능 손실을 유발할 수 있음을 발견
  • useMemo를 사용하여 실제 계산을 캐시하는 경우, 주요 목표는 하위 구성 요소에서 리렌더링되는 것을 방지하는 것이 아니다.

나의 결론

  • useMemo는 처리량이 많을 때 사용해야 한다.

  • The threshold from when useMemo becomes interesting for avoiding extra processing highly depends on your application (이 부분 뭐랄까, 감은 오는데 해석을 못하겠음)

  • 처리량이 매우 낮은 경우 useMemo를 사용하면 사용에 따른 추가 오버헤드가 발생할 수 있음

  • 즉, 처리량이 많아 렌더링에 문제가 되는경우 리렌더시 비용 절감을 위해 사용함.

  • 참조 동일성(Referential equality)을 위해 쓰지 말것!

    • 쓴다면 useRef로 이렇게 쓸듯?
    function Bla() {
      const { current: baz } = useRef([1, 2, 3])
      return <Foo baz={baz} />
    }

여담으로 useMemo를 잘못쓰는 경우 사례

useEffect로 초기값 잡을때 ESLint hook warnings 피하려고 useMemo를 쓰는데 이는 비용면으로 좋지 못하다

function Example ({ impressionTracker, propA, propB, propC }) {
  useEffect(() => {
    // 👇Track initial impression
    impressionTracker(propA, propB, propC)
  }, [])

  return <BeautifulComponent propA={propA} propB={propB} propC={propC} />                 
}

이 잠재적으로 버그를 유발하는 코드를 수정하기 위해 이렇게 useMemo를 쓰는 경우가 있다.

function Example({impressionTracker, propA, propB, propC}) {

  // useMemo to memoize the value i.e so it doesn't change
  const initialTrackingValues = useMemo({
    tracker: impressionTracker, 
    params: {
       propA, 
       propB, 
       propC, 
    }
  }, []) // 👈 you get a lint warning here

  // track impression 
  useEffect(() => {
    const { tracker, params} = initialTrackingValues
    tracker(params)
  }, [tracker, params]) // 👈 you must put these dependencies here

  return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
} 

이러면, 위에서 봤다시피 쓸대없이 렌더링 비용만 늘린다.

이 때는, useRef를 사용하자

function Example({impressionTracker, propA, propB, propC}) {
  // keep reference to the initial values         
  const initialTrackingValues = useRef({
      tracker: impressionTracker, 
      params: {
        propA, 
        propB, 
        propC, 
    }
  })

  // track impression 
  useEffect(() => {
    const { tracker, params } = initialTrackingValues.current;
    tracker(params)
  }, []) // you get NO eslint warnings for tracker or params

  return <BeautifulComponent propA={propA} propB={propB} propC={propC} />   
}

Ref.


Profile picture

박찬희
할 필요가 없는 일은 굳이 만들지 않는 사람.
Github Twitter Newsletter