2021년 8월 1일 • ☕️ 3 min read
본글은 useCallback
, useMemo
에 대해 설명하는 글이 아님을 알린다.
아래 코드를 살펴보자.
const Foo = () => {
const [count, setCount] = useState(0);
// case 1
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, []);
// case 2
const handleIncrement = () => {
setCount(count + 1);
};
return <Box onClick={handleIncrement} />;
};
case1과 case2 중에 비용이 적게 드는 코드는 어느쪽일까? 많은 아티클에서 inline
함수는 useCallback
으로 안에 넣는 것이 성능에 더 좋다 말하고 있기 때문에 case1을 선택할 수 있겠지만 실상은 그렇지 않다.
모든 추상화 및 최적화 코드에는 비용이 들기 마련이다. 이때 발생하는 비용을 상쇄 시킬만한 비용절약이 있지 않으면 오히려 비용 증가가 일어난다. 최적화 관점에서 useCallback
을 사용하기 위해선 전후 성능을 비교 후에 사용하는 것이 옳다. 최근에는 함수가 맡는 롤이 작기 때문에 필자는 대부분 사용하지 않고 있다.
useMemo
는 useCallback
과 어떤 차이가 있을까? 어떠한 타입에 상관 없이 memoization 한다는 것을 제외하면 useCallback
과 굉장히 유사하다. useMemo
의 콜백 함수는 디펜던시 배열에 값이 변화되어야 할때만 재선언 되는 값이 되도록합니다. 매 랜더마다 initialize
배열을 초기화 하기 싫으면 아래와 같이 작성 할 수 있다.
- const initialFruits= ['apple', 'banna', 'coconut']+ const initialFruits = React.useMemo(
+ () => ['snickers', 'skittles', 'twix', 'milky way'],
+ [],
+ )
문제는 해결 됐다. 하지만 이 해결방법은 간단한 코드를 더 복잡하게 만들었고 좋은 코드는 아니다. 이 경우엔 useMemo
를 사용하는 것이 좋지 않을 수 있다. 왜냐하면 또 다시 매렌더마다 useMemo
를 호출하고 있고 메모리를 할당하고 있기 때문이다. 여기선 아래와 같이 작성하는 것이 더 좋은 방법이다.
+ const initialFruits = ['apple', 'banna', 'coconut']
function Fruit() {
- const initialFruits = ['apple', 'banna', 'coconut'] const [fruits, setFruits] = React.useState(initialFruits)
하지만 위와 같은 고민을 하는 것에 시간을 들일 필요 없다. 대부분의 값은 함수안에서 초기화 되거나 prop
으로 부터 오기 때문이다. 이러한 최적화로 부터 오는 이점은 작기 때문에 이러한 고민보다 product를 개선하는데 시간을 들이자.
그럼 도대체 언제 사용하라는 말인가? useMemo
, useCallback
을 사용할 때 많이들 놓치는 부분이 있다. 바로 참조 동일성의 이점을 고려하지 않는다. 아래 코드를 살펴보자.
완성형 코드가 아니니 작성 된 코드의 맥락만 이해하길 바란다.
const Child = ({ bar, baz }) => {
const options = { bar, baz };
useEffect(() => {
handleOptions(options);
}, [options]);
return <Box>foo</Box>;
}
useEffect
는 options
에 대해서 equality 체크를 매 렌더마다 하게 되고, 자바스크립트가 오브젝트 비교 방식 때문에, options
는 매 시간마다 매번 새롭게 만들어진다. 그래서 리액트는 options
가 매 렌더마다 변화 했는지 체크를 하는 테스트를 할때마다 항상 true를 반환하게 됩니다. 이를 해결 하기 위한 방법은 아래와 같다.
const Child = ({ bar, baz }) => {
useEffect(() => {
const options = { bar, baz };
handleOptions(options);
}, [bar, baz]);
return <Box>foo</Box>;
}
아주 간단한 방법이고, 실제로 해결 될 수만 있다면 더할나위 없다. 하지만 문제는 bar
, baz
가 객체일 경우이다. 아래를 살펴 보자.
const Parent = () => {
const bar = () => {};
const baz = { a: 123 };
return <Foo bar={bar} baz={baz} />;
}
bar
, baz
는 이제 오브젝트다. Foo
컴포넌트의 useEffect
는 equality 체크에서 매번 true를 반환하게 되고 매번 렌더링을 다시 하게 됩니다. 문제의 코드를 수정해보자.
const Parent = () => {
const bar = useCallback(() => {}, [])
const baz = useMemo(() => { a: 123 }, [])
return <Child bar={bar} baz={baz} />;
}
이제 bar
, baz
의 레퍼런스를 유지 시켜 equality 체크에서 false를 반환 할 것이다. 최적화의 관점에서 보면 분명 비용증가가 일어 날 수 있다. 하지만 레퍼런스를 유지시 켜준다는 것은 개발자가 의도하지 않은 렌더링으로 부터 사이드이펙트가 발생하는 것을 방지 했을때 가져다주는 이점이 훨씬 크다.
커스텀 훅도 마찬가지다. 외부로 나가는 함수는 참조동일성을 지켜서 밖으로 내보내주는 것이 커스텀훅을 사용하는 개발자를 배려하는 것이다.
useCallback
, useMemo
는 오히려 코드 복잡도를 증가 시킬 수 있고, 디펜던시 배열에서 실수를 유발 할 수 있다. 우리의 코드가 최적화가 되지 않아 퍼포먼스에 크리티컬한 영향이 있는게 아니라면 최적화의 관점 보다 참조 동일성의 관점에서 useCallback
, useMemo
를 사용하고, 최적화가 필요하다면 비용 측정을 먼저 하는것이 선행 돼야 한다.