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의 사용 목적과 양상이 확실히 다르므로 여기서는 둘 다 다룰 것이다.
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로 들어온 값들에 변화가 생길 때에만 함수를 새로 생성하여 사용한다.
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 |
|
|
또한 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의 사용을 고려해야 한다.
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도 리렌더되지 않는다.
}
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도 리렌더되지 않는다.
}
The First of Rule of Optimization:
Don't.
이미 리액트는 여러 최적화 방법을 적용하고 있고, 충분히 빠르다.
그리고 과연 리액트를 개발하는 짱짱맨 개발자들보다 내 능지가 더 뛰어날까?
컴파일러 개발자 못 믿겠다고 어셈블리로 개발하는 셈이 아닐까?
팝콘이나 뜯어야겠다.