Vanilla JS로 React Hook 구현하기 - useMemo

q1q1·2026년 7월 1일

Mini React

목록 보기
4/6

useEffect를 구현하면서 deps 비교, 슬롯 구조를 이미 익혔어서 이번 useMemo를 구현하는데에는 훨씬 수월했다.
useMemo는 구조적으로 useEffect와 매우 비슷하지만, 목적이 다르다.

useEffect와는 어떻게 다를까?

useEffectuseMemo
실행 시점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 유틸을 그대로 재사용했다.

1. useMemo(callback, deps)를 호출하면 callback의 실행 결과값을 반환하기

첫 렌더에는 비교할 이전 deps가 없으니 무조건 callback을 실행하고 결과값을 슬롯에 저장한다.

if (memos[currentIndex] === undefined) {
    const memoizedValue = callback();
    memos[currentIndex] = { deps, memoizedValue };
    return memoizedValue;
}

2. deps가 같을 때 이전 값 그대로 반환하기

deps가 바뀌지 않았다면 callback을 실행하지 않고 슬롯에 저장된 값을 바로 반환한다.

} else {
    return memos[currentIndex].memoizedValue;
}

3. deps가 바뀌었을 때 callback 재실행 후 새 값을 저장하기

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에 useMemodeps와 관련 없는 두 가지 상태를 만들어서 테스트했다.

const [memoA, setMemoA] = useState(0);
const [memoB, setMemoB] = useState(0);  // deps와 무관

const memoizedValue = useMemo(() => {
    console.log("useMemo: 재계산!");
    return memoA * 2;
}, [memoA]);

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

  • memoA 버튼을 눌렀을 때
    ➡️ deps인 [memoA]가 바뀌었으니 callback이 재실행되고 콘솔에 "useMemo: 재계산!"이 찍혀야 한다.

  • memoB 버튼을 눌렀을 때
    ➡️ memoB는 deps에 포함되지 않는다. 상태가 바뀌어 리렌더는 발생하지만, isSameDeps가 true이므로 callback은 실행되지 않고 이전에 저장된 memoizedValue를 그대로 반환해야 한다.

memoA 증가 버튼 → "useMemo: 재계산!" 콘솔 출력 ✅
memoB 증가 버튼 (deps와 무관) → 리렌더는 되지만 재계산 없음 ✅

두 케이스 모두 의도한 대로 동작하는 걸 확인했다! 굿굿

마무리

앞에서 말한 것처럼 useEffect를 구현하면서 deps 비교(shallowEqual)와 슬롯 구조(hookIndex)를 이미 익혀둔 덕분에 useMemo는 훨씬 빠르게 구현할 수 있었다.

핵심은 "렌더 중에 실행되고 값을 반환한다" 는 점인 것 같다.
이 차이 때문에 useEffect에서 필요했던 pendingEffects[] 큐가 전혀 필요 없었다.

다음엔 useCallback을 구현해볼 예정인데, 사실 useCallback(fn, deps)useMemo(() => fn, deps)와 구조가 동일하다. useMemo를 이해했다면 useCallback은 금방이겠...지? 😄😄😄

profile
끄적끄적

0개의 댓글