_.memoize 소스코드


Lodash 라이브러리의 Memoize 메서드를 사용하면 이전에 진행했던 연산 결과를 재사용할 수 있다. 실시간 검색창을 구현할 때 입력한 키워드에 대한 API 호출을 시도하는데, 이미 검색했던 키워드는 결과를 캐싱해놓고 재사용하면 API 중복 호출을 방지할 수 있다. 이때 Lodash의 Memoize를 활용할 수 있다(물론 키워드에 대한 결과값이 자주 변한다면 캐싱 기능을 사용할 필요 없다)

import _ from 'lodash';

export const requestQuotes = _.memoize(async title => {
    const res = await fetch(`https://animechan.vercel.app/api/quotes/anime?title=${title}`)
    if(res.status !== 200) return [];

    const quotesArray = await res.json();
    return quotesArray;
});

requestQuotes('china') // 'china' 키워드에 대한 API 호출 결과 캐싱(Map의 'china' key에 저장)
requestQuotes.cache // Map(1) { 'china' -> ...} 대충 이런 모양

Memoize 메서드의 캐싱을 어떻게 구현하고 어디에 저장되는지 궁금해서 소스코드를 찾아봤다. 결론적으로 Map과 클로저를 활용해서 구현했다. memoize 함수의 첫번째 콜백이 받는 파라미터는 Map(캐시)의 key로 사용된다. Map은 문자, 숫자 같은 원시형은 물론 참조형(함수, 객체 등)도 key로 사용할 수 있기 때문에 Map을 활용하는 것 같다.

function memoize(func, resolver) {
  if (
    typeof func !== 'function' ||
    (resolver != null && typeof resolver !== 'function')
  ) {
    throw new TypeError('Expected a function');
  }
  const memoized = function (...args) {
    const key = resolver ? resolver.apply(this, args) : args[0];
    const { cache } = memoized;

    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = func.apply(this, args);
    memoized.cache = cache.set(key, result) || cache;
    return result;
  };

  memoized.cache = new (memoize.Cache || Map)();
  return memoized;
}

memoize.Cache = Map; 

참고로 memoize.Cache 프로퍼티는 Map 생성자 함수를 참조하도록 작성되어 있다. 이유는 잘 모르겠다.

memoize.Cache === Map // true

작동 방식


자바스크립트에서 함수는 일급 객체이므로, 프로퍼티를 추가/삭제 할 수 있다. 함수는 기본적으로 length(파라미터 갯수)와 prototype(생성자 함수일 때) 프로퍼티를 가진다. 익명함수가 아니면 name 프로퍼티도 가진다.

const add10 = (num) => num + 10;
Object.getOwnPropertyDescriptors(add10)
// length: {value: 1, writable: false, enumerable: false, configurable: true}
// name: {value: 'add10', writable: false, enumerable: false, configurable: true}

add10.desc = '숫자를 인자로 받아 10을 더한 값을 반환하는 함수'
console.log(add10.desc) // '숫자를 인자로 받아 10을 더한 값을 반환하는 함수'

delete add10.desc // true
console.log(add10.desc) // undefined

<aside> 💡 memoize() 파라미터에 resolver 함수를 넘기면, resolver가 반환하는 값을 캐시의 key로 사용한다.

</aside>

숫자를 인자로 받아 10을 더한 값을 반환하는 add10 함수를 memoize의 첫번째 파라미터로 넘긴다고 가정해본다. memoize 함수가 반환하는 함수는 cache(Map 객체)라는 프로퍼티를 가지며, 여기에 캐싱될 값들이 담긴다. 파라미터로 넘겼던 add10 함수는 memoize 함수의 지역 변수로 등록돼서 사용된다(클로저).

const add10 = (num) => num + 10;
const cachedValue = memoize(add10);

console.log(cachedValue) // ƒ (...args) {...}
console.log(cached.cache) // Map(0) {size: 0}

cachedValue 파라미터에 8을 넘겨 실행했다고 가정하면...

cachedValue(8)
  1. key 획득. 제공된 resolver 함수가 없으므로 파라미터로 받은 8key가 된다(args는 [8])
  2. cache(Map) 객체에 8이라는 key가 있는지 확인
    1. 있으면 해당 key(8)의 값을 반환하고
    2. 없으면 다음 단계 진행