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

skdus·2022년 5월 1일
1

JavaScript

목록 보기
6/17
post-thumbnail

💡 decorator

함수를 인자로 받고, 그 함수의 행동을 변경시켜서 반환하는 함수를 데코레이터(decorator) 라고 한다.

function slow(x) {
  // many operations
   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) ); // 윗줄과 동일한 결과

연산이 매우 많지만 안정적인(input과 ouput이 1대1) 함수 slow(x)가 있다고 가정했을 떄, slow() 안에 캐싱 관련 코드를 추가하는 대신, 래퍼 함수를 만들어서 캐싱 기능을 추가하고 있다.

  • 모든 함수를 대상으로 cachingDecorartor 를 적용할 수 있기 때문에 유용하다.(재사용성)
  • 캐싱 관련 코드를 함수로 분리할 수 있어 slow 자체의 복잡성이 증가하지 않는다
  • Decorator 적용 전과 후의 결과가 동일하다.

💡 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.someMethod 접근에 실패했기 때문에 에러가 발생한다. 원인은 (**) 로 표시한 줄에서 래퍼가 기존 함수 func(x) 를 호출하면 this 가 undefined 가 되기 때문이다.

그래서 이 에러를 해결하기 위해서는 this 를 명시적으로 고정해서 우리가 의도한 것처럼 동작하도록 하는 작업이 필요하다. 이 때 사용 가능한 것이 func.call(context, ...args) 이다.

func(1, 2, 3);
func.call(obj, 1, 2, 3);

위의 두 코드는 거의 동일한 일이 발생한다.

둘 다 인수로 1,2,3 을 받는데 차이점은 func.call에서는 this가 obj 로 고정된다는 것이다.

다른 컨텍스트에서 sayHi 를 호출하는 예시를 보면 sayHi.call(user) 를 호출하면 sayHi의 컨텍스트가 this=user 로, sayHi.call(admin) 을 호출하면 sayHi의 컨텍스트가 this=admin 으로 설정된다.

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

그래서 call 메서드를 이용하면 데코레이터 함수를 다음과 같이 수정할 수 있다.

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;
  };
}

💡 func.apply

이번에는 인자가 여러개인 함수를 캐싱처리 해야하는 상황이라고 가정하면, 여러개의 인자를 하나의 키 값으로 만들어주는 hash 함수와 func.call(this, x) 와 같이 사용했던 부분을 func.call(this, ...arguments) 로 교체해서 래퍼 함수로 감싼 함수가 호출될 때 복수 인수를 넘길 수 있도록 변경해야한다.

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.call(this, ...arguments) 대신 func.apply(this, arguments) 를 사용할 수 있다.

func.apply(context, args);

apply는 func의 this를 context로 고정해주고, 유사 배열 객체인 args를 인수로 사용할 수 있게 해준다. 이 때 call과 apply의 문법적인 차이는 call이 복수 인수를 따로 받는 대신 apply는 인수를 유사 배열 객체로 받는다는 점이다. 따라서 아래 코드 두 줄은 거의 같은 역할을 한다.

func.call(context, ...args);
func.apply(context, args);
  • 전개 문법 ...은 이터러블 args을 분해 해 call에 전달할 수 있도록 해준다.
  • apply는 오직 유사 배열 형태의 args만 받는다.

이 차이만 빼면 두 메서드는 완전히 동일하게 동작한다. 인수가 이터러블 형태라면 call을, 유사 배열 형태라면 apply를 사용하면 된다.

이렇게 컨텍스트와 함께 인수 전체를 다른 함수에 전달하는 것을 콜 포워딩(call forwarding) 이라고 부른다.


💡 요약

  • 데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킨다. 주요 작업은 여전히 함수에서 처리한다.
  • func.call(context, arg1, arg2, ...) - 주어진 컨텍스트와 인수를 사용해 func 를 호출한다.
  • func.apply(context, args) - this에 context가 할당되고, 유사 배열 args가 인수로 전달되어 func이 호출된다.
  • 콜 포워딩은 보통 apply를 사용해서 구현한다.

참고자료

0개의 댓글