320x100
320x100

코드 변경 없이 캐싱 기능 추가하기

: 자주 호출되는 함수의 결과를 캐싱하여 재연산에 걸리는 시간을 줄여야함

: 함수 내부에 캐싱 관련 코드를 추가하는 대신 래퍼 함수를 만들어 캐싱 기능을 추가하면 이점이 많음

function slow(x) {
  // CPU 집약적인 작업이 여기에 올 수 있습니다.
  alert(`slow(${x})을/를 호출함`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // cache에 해당 키가 있으면
      return cache.get(x); // 대응하는 값을 cache에서 읽어옵니다.
    }

    let result = func(x);  // 그렇지 않은 경우엔 func를 호출하고,

    cache.set(x, result);  // 그 결과를 캐싱(저장)합니다.
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1)이 저장되었습니다.
alert( "다시 호출: " + slow(1) ); // 동일한 결과

alert( slow(2) ); // slow(2)가 저장되었습니다.
alert( "다시 호출: " + slow(2) ); // 윗줄과 동일한 결과

 

- 데코레이터

: 인수로 받은 함수의 행동을 변경 시켜주는 함수

: 함수에 위 예시의 cachingDecorator를 적용하기만 하면 캐싱이 가능한 함수를

  원하는 만큼 구현할 수 있기 때문에 유용하게 사용되며 코드가 간결해질 수 있음

: 재사용할 수 있으며 캐싱 로직이 분리되어 로직의 복잡성이 증가하지 않음

 

 

 

 

 

func.call을 사용해 컨텍스트를 지정

: 데코레이터의 경우 객체 메서드에 사용하기에 적합하지 않음

// worker.slow에 캐싱 기능을 추가해봅시다.
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // CPU 집약적인 작업이라 가정
    alert(`slow(${x})을/를 호출함`);
    return x * this.someMethod(); // (*)
  }
};

// 이전과 동일한 코드
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // 기존 메서드는 잘 동작합니다.

worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용

alert( worker.slow(2) ); // 에러 발생!, Error: Cannot read property 'someMethod' of undefined

: 래퍼가 기존 메서드 호출 결과를 전달하려 했지만 this의 컨텍스트가 사라졌기 때문에 에러가 발생

: func.call을 사용하면 this를 멸시적으로 고정해 함수를 호출할 수 있음

func.call(context, arg1, arg2, ...)
function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// call을 사용해 원하는 객체가 'this'가 되도록 합니다.
sayHi.call( user ); // this = John
sayHi.call( admin ); // this = Admin



function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// this엔 user가 고정되고, "Hello"는 메서드의 첫 번째 인수가 됩니다.
say.call( user, "Hello" ); // John: Hello

 

 

let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert(`slow(${x})을/를 호출함`);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // 이젠 'this'가 제대로 전달됩니다.
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용

alert( worker.slow(2) ); // 제대로 동작합니다.
alert( worker.slow(2) ); // 제대로 동작합니다. 다만, 원본 함수가 호출되지 않고 캐시 된 값이 출력됩니다.

 

 

 

 

 

여러 인수를 전달하여 cachingDecorator를 다채롭게 만들기

: 위 방법들은 인수가 여러 개인 함수에는 사용할 수 없음

: Map은 단일키만 받기 때문

 

- 해결방법들

: 복수 키를 지원하는 맴과 유사한 자료구조 구현 (서드파티 라이브러리 등 사용)

: 중첩 맵 사용

> max, result 쌍은 cache.set(min) / cache.get(min).get(max) 이런식으로

: 두 값을 하나로 합치기

> 해싱함수를 구현하여 유연성까지 확보

 

- 두 값을 하나로 합치는 방식의 다중 인수 데코레이터

: 래퍼 함수로 감싼 함수가 호출될 때 복수 인수를 넘길 수 있도록 수정

let worker = {
  slow(min, max) {
    alert(`slow(${min},${max})을/를 호출함`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // 제대로 동작합니다.
alert( "다시 호출: " + worker.slow(3, 5) ); // 동일한 결과 출력(캐시된 결과)

 

 

 

 

func.apply

: func의 this를 context로 고정해주고 유사 배열 객체인 args를 인수로 만들 수 있게 해주는 유연한 메서드

func.apply(context, args)


// 비교
func.call(context, ...args); // 전개 문법을 사용해 인수가 담긴 배열을 전달하는 것과
func.apply(context, args);   // call을 사용하는 것은 동일합니다.

: apply는 오직 유사 배열 형태의 arg만 받음

: 인수가 이터러블의 형태라면 call을, 유사 배열 형태라면 apply를 사용

: 자바스크립트 엔진 내부에서는 apply를 최적화 하기 때문에 apply를 권장

 

- 콜 포워딩

: 컨텍스트와 함께 인수 전체를 다른 함수에 전달하는 것

let wrapper = function() {
  return func.apply(this, arguments);
};

 

 

 

 

메서드 빌리기

: arr.join을 활용하여 위 캐싱 함수의 해싱 함수를 개선

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

 

 

 

 

데코레이터와 함수 프로퍼티

: 함수를 데코레이터로 감싸 대체하는 것은 대체적으로 안전하지만, 

  원본함수에 func.calledCount 등의 프로퍼티가 있는 경우 

  데코레이터를 적용한 함수에서 프로퍼티를 사용할 수 없으므로 안전하지 않음

 : 함수에 프로퍼티가 있는 경우에는 사용에 주의 필요

 

 

 

 

 

 

Refference

 

call/apply와 데코레이터, 포워딩

 

ko.javascript.info

 

300x250
728x90