이번 시간에는 useMemo hook을 배워봅시다. useCallback과 많이 헷갈려하시는 Hook입니다. 하지만 사용하는 곳은 전혀 다르므로 실제로는 헷갈려하시면 안 됩니다.
간단히 요약하자면 useCallback은 함수 자체를 캐싱하고, useMemo는 값을 캐싱합니다. 물론 값이 함수일 수는 있겠지만요. 헷갈리는 이유는 다음과 같은 형식 때문인 것 같습니다.
useCallback(() => {}, []);
useMemo(() => 값, []);
둘 다 함수를 첫 번째 인자로 받고, 두 번째 인자로는 deps 배열을 받습니다. 하지만 useCallback은 () => {} 자체를 캐싱하는 것이고, useMemo는 값만 캐싱하게 됩니다. 캐싱한 값을 바꾸고 싶을 때는 deps 배열을 사용하면 되겠죠. deps 배열 내부의 값이 달라지면 기존 캐싱된 것을 버리고 새로 값을 구합니다.
예를 들어 로또 추첨 번호를 계산하는 getWinNumbers 함수가 있다고 칩시다.
function getWinNumbers() {
console.log('getWinNumbers');
const candidate = Array(45).fill().map((v, i) => i + 1);
const shuffle = [];
while (candidate.length > 0) {
shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
}
const bonusNumber = shuffle[shuffle.length - 1];
const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c);
return [...winNumbers, bonusNumber];
}
이 함수를 컴포넌트 안에 state로 넣으면 어떻게 될까요?
const Basic = () => {
const [lottoNumbers, setLottoNumbers] = useState(getWinNumbers());
return <div>{lottoNumbers.join(',')}</div>;
};
이런 식이 되겠죠.
문제는 이 컴포넌트가 리렌더링 될 때마다 getWinNumbers가 자꾸 다시 호출됩니다. 물론 useState는 리렌더링 시 다시 호출되더라도 무시하므로 최종적으로는 state나 화면에는 아무 영향이 없긴 하지만요.
getWinNumbers 함수가 매우 비용이 큰 연산을 하는 함수라면 문제가 될 수 있습니다. 화면에서 바뀌는 것은 없지만 리렌더링 될 때마다 계속 돌아가긴 하는 것이니까요. 특히 자식 컴포넌트는 부모 컴포넌트가 리렌더링되면 따라서 리렌더링되므로 리렌더링 조절 권한이 없는 경우도 많습니다.
이럴 때 useMemo를 사용해 getWinNumbers() 결괏값을 캐싱해주는 것이 좋습니다.
const Basic = () => {
const cachedNumbers = useMemo(() => getWinNumbers(), []);
const [lottoNumbers, setLottoNumbers] = useState(cachedNumbers);
return <div>{lottoNumbers.join(',')}</div>;
};
이제 Basic 컴포넌트가 리렌더링되더라도 getWinNumbers가 다시 호출되는 일은 없을 겁니다. deps 배열에 든 값이 바뀌기 전에는요. 그런데 deps 배열 안에 요소가 아무것도 없으니 평생 바뀔 일은 없겠군요.
이렇게 연산 비용이 복잡한 함수는 useMemo로 캐싱하시면 됩니다. 효율성 측면에서 필요한 Hook이 useMemo입니다.
다음 시간에는 class 컴포넌트의 라이프사이클을 대체하는 useEffect hook에 대해 알아보도록 하겠습니다.