When NOT to use useMemo & useCallback

PeaceSong·2021년 9월 19일
2
post-custom-banner

0. Intro

useMemo: https://reactjs.org/docs/hooks-reference.html#usememo
useCallback: https://reactjs.org/docs/hooks-reference.html#usecallback

useMemo와 useCallback 모두 시간을 줄이기 위해 (CPU 연산을 줄이기 위해) 메모리를 희생하는 최적화 방법이다. 사실 useCallback(fn, deps)useMemo(() => fn, deps)가 같다는 점에서 useMemo에 해당되는 내용은 useCallback에도 해당되지만, 두 hook의 사용 목적과 양상이 확실히 다르므로 여기서는 둘 다 다룰 것이다.

1. 왜 useMemo와 useCallback를 쓸까?

useMemo는 함수의 실행 결과값을 memoize하여 저장한다.

const memoizedValue = useMemo(() => {
  // do something expensive
  return someValue(a, b, c)
}, [a, b, c])

이게 다다. 단지 dependencay array로 들어온 값들에 변화가 생길 경우에만 memoizedValue가 다시 계산될 뿐이다. 따라서 useMemo는 첫 번째 인자로 들어오는 함수가 매우 비싼 함수일 때 가장 잘 쓰이는 것이다.

useCallback은 함수 자체를 memoize하여 저장한다.

const memoizedFunc = useCallback((d) => {
  // do things
  someBehaviors(a, b, c, d)
  return something
}, [a, b, c])

useCallback 또한 dependancy array로 들어온 값들에 변화가 생길 때에만 함수를 새로 생성하여 사용한다.

2. 그러면 언제 useMemo와 useCallback를 쓰면 안될까?

2.1. memoization

Memoization: an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. (Wikipedia)

위 Memoization의 정의에 부합되지 않는 모든 useMemo와 useCallback 사용은 지양되어야 한다! 3000번째 피보나치 수를 찾거나, 수십 개의 데이터에 대해 .map 함수로 뭔가 새로운 객체를 우루루 만들어내는 행동, 또는 굉장히 긴 body를 가진 함수를 만드는 것은 useMemo와 useCallback의 취지와 부합하니 사용해도 좋다(비싸니까). 하지만 props로 넘겨 줄 객체를 만드는 게 비싸지 않음에도 useMemo로 만들어 넘기거나, initial state를 useMemo로 기억하고 있는 것은 비싼 함수의 결과값이나 동작을 기억하고 있는 것이 아니므로 해서는 안된다.

Bad Good
const Foo: FC = () => {
  // whatever blah blah blah
  
  const someProps: SomeProps = useMemo(() => ({
    field0: 1,
    field1: someString,
    field2: someObj
  }), [someString, someObj])
  // 단순히 객체를 뱉는 함수는 전혀 비싸지 않다.
  
  return (
    <Bar someProps={someProps} />
  )
}
const Foo: FC = () => {
  // whatever blah blah blah
  
  const someProps: SomeProps = {
    field0: 1,
    field1: someString,
    field2: someObj
  }
     
  return (
    <Bar someProps={someProps} />
  )
}

2.2. garbage collection

또한 useMemo를 통해 memoize한 데이터는 garbage collection에서 제외된다. 평범하게 변수할당을 통해 가지고 있는 데이터라면 다음 렌더에서 새 객체로 할당되고 기존 객체는 정상적으로 반환되겠지만, useMemo로 할당된 데이터는 컴포넌트가 언마운트되지 않는 한 메모리 어느 구석에서 자리를 차지하게 된다.

useCallback의 예시를 보자.

const _blockScroll = () => {
  document.body.position = 'fixed'
  document.body.overflow = 'hidden'
}
const blockScroll = useCallback(_blockScroll, [])

위 코드의 동작 자체는 문제가 없다. blockScroll은 매 렌더 시에도 동일한 함수 객체를 가리키고 있을 것이다. 하지만 두 번째 렌더부터는 useCallback에 묶인 _blockScroll 객체는 수거되지 않을 것이고, 그와 별개로 새 _blockScroll 객체는 여전히 생성되어 메모리를 차지하게 될 것이다. 이 오버헤드를 상쇄할 만큼의 성능 상 이득이 있을 때에만 useMemo와 useCallback의 사용을 고려해야 한다.

3. 이제 referential equality는 누가 보장해주냐?

3.1. referentially equal한 값이 다른 변수에 의해 변하는 값일 때

const Foo: FC = () => {
  // whatever blah blah blah
  
  const someProps: SomeProps = {
    field0: 1,
    field1: someString,
    field2: someObj
  }
     
  return (
    <ExpensiveBar someProps={someProps} />
  )
}

어떤 이유로든 Foo가 리렌더되면 someProps는 재할당되어 reference하는 값이 달라질 것이고, 따라서 ExpensiveBar도 리렌더될 것이다. 그런데 문제는 ExpensiveBar가 비싸다면 someProps가 가리키는 값이 실제로 같든 다르든 비싼 값을 치르고 리렌더해야 할 것이다. 이건 우리가 전혀 바라는 바가 아니다.

이럴 때는 useMemo로 referential equality를 보장해줄 수 있다. someProps를 메모해둠으로써 리렌더에 상관없이 늘 같은 객체를 가리키게 하는 것이다.

const Foo: FC = () => {
  // whatever blah blah blah
  
  const someProps: SomeProps = useMemo(() => ({
    field0: 1,
    field1: someString,
    field2: someObj
  }), [someString, someObj]) 
  // someString, someObj에 대한 reference가 변하지 않는 한 someProps에 대한 reference가 변하지 않는다.
     
  return (
    <ExpensiveBar someProps={someProps} />
  ) // 따라서 ExpensiveBar도 리렌더되지 않는다.
}

3.2. referentially equal한 값이 다른 변수에 의해 변하지 않는 값일 때 (상수일 때)

useRef 쓰세요.

const Foo: FC = () => {
  // whatever blah blah blah
  
  const somePropsRef = useRef<SomeProps>({
    field0: 1,
    field1: 'hello, world!',
    field2: {}
  })
     
  return (
    <ExpensiveBar someProps={somePropsRef.current} />
  ) // somePropsRef.current가 변하지 않으므로 ExpensiveBar도 리렌더되지 않는다.
}

4. 킹치만... 최적화는 하고 싶은 걸...

The First of Rule of Optimization:
Don't.

이미 리액트는 여러 최적화 방법을 적용하고 있고, 충분히 빠르다.
그리고 과연 리액트를 개발하는 짱짱맨 개발자들보다 내 능지가 더 뛰어날까?
컴파일러 개발자 못 믿겠다고 어셈블리로 개발하는 셈이 아닐까?

팝콘이나 뜯어야겠다.

profile
127.0.0.1
post-custom-banner

0개의 댓글