useEffect를 구현하면서 deps 비교, 슬롯 구조를 이미 익혔어서 이번 useMemo를 구현하는데에는 훨씬 수월했다.
useMemo는 구조적으로 useEffect와 매우 비슷하지만, 목적이 다르다.
| useEffect | useMemo | |
|---|---|---|
| 실행 시점 | DOM 갱신 후 | 렌더링 중 |
| 반환값 | cleanup 함수 | 메모이제이션된 값 |
| 큐 필요? | ✅ pendingEffects[] | ❌ 즉시 반환 |
useMemo는 렌더 중에 값을 바로 써야 하기 때문에, useEffect에서 구현한 pendingEffects[]같은 큐가 필요 없다. 그냥 계산하고 바로 반환하면 된다. 다만 useEffect에서 effects[]에 deps와 cleanup을 담은 것처럼 useMemo도 deps와 callback을 실행한 값을 담아준다.
useMemo(callback, deps)를 호출하면 callback의 실행 결과값을 반환한다.deps가 바뀌지 않으면 이전에 계산한 값을 그대로 반환한다. (재계산 없음)deps가 바뀌면 callback을 다시 실행해 새 값을 반환한다.확실히 useEffect 구현할 때 고민 많이 하다가 useMemo를 고민하려니까 구현할 내용도 비교적 수월했다! ㅎㅎㅎ
deps 비교는 useEffect 구현 때 만든 shallowEqual 유틸을 그대로 재사용했다.
첫 렌더에는 비교할 이전 deps가 없으니 무조건 callback을 실행하고 결과값을 슬롯에 저장한다.
if (memos[currentIndex] === undefined) {
const memoizedValue = callback();
memos[currentIndex] = { deps, memoizedValue };
return memoizedValue;
}
deps가 바뀌지 않았다면 callback을 실행하지 않고 슬롯에 저장된 값을 바로 반환한다.
} else {
return memos[currentIndex].memoizedValue;
}
deps가 달라졌다면 callback을 다시 실행해 새 값을 슬롯에 업데이트한다.
} else if (!isSameDeps) {
const memoizedValue = callback();
memos[currentIndex] = { deps, memoizedValue };
return memoizedValue;
}
전체적인 코드를 본다면 이런 느낌!!
import { getNextHookIndex } from "./hookCore";
import { shallowEqual } from "../utils/equality";
// memos[]: 렌더 간 deps/memoizedValue 유지를 위한 저장소
let memos = [];
export function useMemo(callback, deps) {
const currentIndex = getNextHookIndex();
// 얕은 비교를 통해 deps가 같은 것인지 확인
const isSameDeps = shallowEqual(memos[currentIndex]?.deps, deps);
// 비교할 이전 deps가 없는 경우 무조건 실행
if (memos[currentIndex] === undefined) {
const memoizedValue = callback();
memos[currentIndex] = { deps, memoizedValue };
return memoizedValue;
// deps가 달라졌다면 callback을 재실행해 새 값을 저장
} else if (!isSameDeps) {
const memoizedValue = callback();
memos[currentIndex] = { deps, memoizedValue };
return memoizedValue;
// deps가 같다면 이전에 계산한 값을 그대로 반환
} else {
return memos[currentIndex].memoizedValue;
}
}
구현했으니 검증해보자!
main.js에 useMemo의 deps와 관련 없는 두 가지 상태를 만들어서 테스트했다.
const [memoA, setMemoA] = useState(0);
const [memoB, setMemoB] = useState(0); // deps와 무관
const memoizedValue = useMemo(() => {
console.log("useMemo: 재계산!");
return memoA * 2;
}, [memoA]);

버튼도 두 개로 나눠서 각각 memoA, memoB를 증가시키도록 했다.



memoA 증가 버튼 → "useMemo: 재계산!" 콘솔 출력 ✅
memoB 증가 버튼 (deps와 무관) → 리렌더는 되지만 재계산 없음 ✅
두 케이스 모두 의도한 대로 동작하는 걸 확인했다! 굿굿
앞에서 말한 것처럼 useEffect를 구현하면서 deps 비교(shallowEqual)와 슬롯 구조(hookIndex)를 이미 익혀둔 덕분에 useMemo는 훨씬 빠르게 구현할 수 있었다.
핵심은 "렌더 중에 실행되고 값을 반환한다" 는 점인 것 같다.
이 차이 때문에 useEffect에서 필요했던 pendingEffects[] 큐가 전혀 필요 없었다.
다음엔 useCallback을 구현해볼 예정인데, 사실 useCallback(fn, deps)는 useMemo(() => fn, deps)와 구조가 동일하다. useMemo를 이해했다면 useCallback은 금방이겠...지? 😄😄😄