본문 바로가기

모던 자바스크립트

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

📢 자바스크립트는 함수를 다룰 때 탁월한 유연성을 제공한다. 함수는 이곳저곳 전달될 수도 있고, 객체로도 사용될수 있다. 

 

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

◾️ CPU를 많이 잡아먹지만 결과는 안정적(x 가 같으면 호출결과도 같다)인 함수 slow(x) 

◾️ slow(x)가 자주 호출된다면, 결과를 어딘가에 저장(캐싱)해 재연산에 걸리는 시간을 줄일 수 있다. 

◾️ 래퍼 함수를 만들어 캐싱 기능을 추가한다. 

function slow(x) {
  // CPU 집약적인 작업
  console.log(`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);

console.log( show(1) ); // show(1)가 저장됨
console.log( '다시 호출: ' + slow(1) ); // 동일한 결과

console.log( show(2) ); // slow(2)가 저장됨
console.log( '다시 호출: ' + slow(2) ); // 동일한 결과

◾️ cachingDecorator같이 인수로 받은 함수의 행동을 변경시켜주는 함수데코레이터(decorator) 라고 한다. 

◾️ 모든 함수를 대상으로 cachingDecorator를 호출 할 수 있는데, 이때 반환되는 것은 캐싱 래퍼이다. 

◾️ 함수에 cachingDecorator를 적용하기만 하면 캐싱이 가능한 함수를 원하는 만큼 구현할 수 있다. 

◾️ 캐싱 관련 코드를 함수 코드와 분리할 수 있기 때문에 함수의 코드가 간결해진다. 

 

◾️ cachingDecorator(func)를 호출하면 래퍼(wrapper), function(x)가 반환된다. 

◾️ 래퍼 function(x)는 func(x)의 호출 결과를 캐싱 로직으로 감싼다. (wrapping)

◾️ 함수 slow는 래퍼로 감싼 이전이나 이후나 동일한 일을 수행한다. 행동 양식에 캐싱 기능이 추가된 것뿐

 

◾️ slow 본문을 수정하는 것 보다 독립된 래퍼 함수 cachingDecorator를 사용할 때 장점

  • cachingDecorator를 재사용 할 수 있다. 원하는 함수 어디에든 cachingDecorator를 적용할 수 있다. 
  • 캐싱 로직이 분리되어 slow 자체의 복잡성이 증가하지 않는다. 
  • 필요하다면 여러 개의 데코레이터를 조합해서 사용할 수도 있다. (추가 데코레이터는 cachingDecorator뒤를 따른다.)

 

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

◾️ 객체 메서드 worker.slow()는 데코레이터 적용 후 제대로 동작하지 않는다. 

◾️ worker.slow()에 캐싱 기능 추가 

let worker = {
  someMethod() {
    return 1;
  },
  
  slow(x) {
    // CPU 집약적인 작업
    console.log(`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;
  };
}

console.log( worker.slow(1) );
worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용
console.log( worker.slow(2) ); // Error: Cannot read property 'someMethod' of undefined

◾️ (*)로 표시한 줄에서 this.someMethod 접근에 실패했기 때문에 에러가 발생 

◾️ 원인은 (**)로 표시한 줄에서 래퍼가 기존 함수 func(x)를 호출하면 this가 undefined가 되기 때문이다.

 

◾️ func.call(context, ...args) : this를 명시적으로 고정해 함수를 호출할 수 있게 해주는 특별한 내장 함수 메서드

func.call(context, arg1, arg2, ...);

◾️ 메서드를 호출하면 메서드의 첫 번째 인수가 this, 이어지는 인수가 func의 인수가 된 후, func가 호출된다. 

◾️ 둘다 인수로 1, 2, 3을 받지만 func.call에선 this가 obj로 고정된다. 

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

 

◾️ 다른 컨텍스트(객체) 하에 sayHi를 호출

◾️ sayHi.call(user)를 호출하면 sayHi의 컨텍스트가 this=user로, sayHi.call(admin)을 호출하면 컨텍스트가 this=admin로 설정

function sayHi() {
  console.log(this.name);
}

let user = { name: 'suzu' };
let admin = { name: 'Admin' };

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

◾️ 컨텍스트와 phrase에 원하는 값을 지정

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

let user = { name: 'suzu' }

// this=user
say.call( user, 'Hello' ); // suzu: Hello

◾️ 래퍼 안에서 call을 사용해 컨텍스트 원본 함수로 전달하면 에러가 발생하지 않는다. 

let worker = {
  someMethod() {
    return 1;
  },
  
  slow(x) {
    // CPU 집약적인 작업
    console.log(`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); // 캐싱 데코레이터 적용

console.log( worker.slow(2) ); // 동작 OK
console.log( worker.slow(2) ); // 동작 OK -> 원본 함수 호출 X, 캐시된 값이 출력

◾️ this가 전달되는 과정

  1. 데코레이터를 적용한 후에 worker.slow는 래퍼 function (x) { ... }이 된다. 
  2. worker.slow(2)를 실행하면 래퍼는 2를 인수로 받고, this=worker가 된다.(점 앞의 객체)
  3. 결과가 캐시되지 않은 상황이라면 func.call(this. x)에서 현재 this (=worker)와 인수(=2)를 원본 메서드에 전달한다. 

 

여러 인수 전달하기

◾️ 복수 인수를 가진 메서드, worker.slow 캐싱

let worker = {
  slow(min, max) {
    return min + max; // CPU를 아주 많이 쓰는 작업이라고 가정
  }
};

// 동일한 인수를 전달했을 때 호출 결과를 기억할 수 있어야 한다. 
worker.slow = cachingDecorator(worker.slow);

◾️ 해결방법

  1. 복수 키를 지원하는 맵과 유사한 자료 구조 구현하기(서드 파티 라이브러리 등을 사용해 됨)
  2. 중첩 맵을 사용하기. (max, result) 쌍 저장은 cache.set(min)으로, resultcache.get(min).get(max)를 사용해 얻는다. 
  3. 두 값을 하나로 합치기. Map의 키로 문자열 min, max를 사용한다. 여러 값을 하나로 합치는 코드는 해싱 함수(hashing function)에 구현해 유연성을 높인다. 

◾️ func.call(this. x)를 func.call(this, ...arguments)로 교체해, 래퍼 함수로 감싼 함수가 호출할 때 복수 인수로 넘길 수 있다. 

let worker = {
  slow(min, max) {
    console.log(`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);

console.log( worker.slow(3, 5) ); 8
console.log( '다시 호출: ' + worker.slow(3, 5) ); // 8캐시된 결과

 

◾️ (*)로 표시한 줄에서 hash가 호출되면서 arguments를 사용한 단일 키가 만들어진다. (좀 더 복잡한 경우라면 또 다른 해싱 함수가 필요할 수 있다. )

◾️ (**)로 표시한 줄에선 func.call(this, ...arguments)를 사용해 컨텍스트(this)와 래퍼가 가진 인수 전부(...arguments)를 기존 함수에 전달하였다. 

 

 

func.apply

◾️ func.call(this, ...arguments) 대신, func.apply(this, arguments)를 사용해도 된다. 

◾️ func.apply문법

func.apply(context, args);

◾️ apply는 func의 this를 context로 고정해주고, 유사 배열 객체인 args를 인수로 사용할 수 있게 해준다. 

◾️ call과 apply의 문법적 차이는 call복수 인수를 따로따로 받는 대신 apply인수를 유사 배열 객체로 받는다.

◾️ 전개 문법을 사용해 인수가 담긴 배열을 전달하는 것과 call을 사용하는 것은 동일하다.   

      ◾️ 전개 문법 ...은 이터러블 args를 분해 해 call에 전달할 수 있도록 해준다. 

      ◾️ apply는 오직 유사 배열 형태의 args만 받는다. 

func.call(context, ...args); 
func.apply(context, args);

◾️ 인수가 이터러블 형태라면 call, 유사 배열 형태라면 apply를 사용하면 된다. 

◾️ 배열 같이 이터러블이면서 유사 배열인 객체엔 둘 다를 사용할 수 있는데, 대부분의 자바스크립트 엔진은 내부에서 apply를 최적화 하기 때문에 apply를 사용하는게 좀 더 빠르다. 

◾️ 콜 포워딩(call forwarding) : 컨텍스트와 함께 인수 전체를 다른 함수에 전달하는 것

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

◾️ 외부에서 wrapper를 호출하면, 기존 함수인 func를 호출하는 것과 명확하게 구분할 수 있다. 

 

 

메서드 빌리기

◾️ 해싱 함수 개선

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

◾️ args의 요소 개수에 상관없이 요소를 합치기 → 배열 메서드 arr.join을 사용

function hash(args) {
  return args.join(); // Error: arguments.join is not a function
}

◾️ hash(arguments)를 호출할 때 인수로 넘겨주는 arguments는 진짜 배열이 아니고 이터러블 객체나 유사 배열 객체이기 때문에 에러가 발생한다. 

◾️ 메서드를 빌려와서 문제 해결 method borrowing

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

hash(1, 2);

◾️ 일반 배열에서 join 메서드를 빌려오고([].join), [].join.call를 사용해 arguments를 컨텍스트로 고정한 후 join메서드를 호출한다.

◾️ arr.join(glue)의 내부 알고리즘

   1. glue가 첫 번째 인수가 되도록 한다. 인수가 없으면 ','가 첫 번째 인수가 된다. 

   2. result는 빈 문자열이 되도록 초기화 한다. 

   3. this[0]을 result에 덧붙인다.

   4. glue와 this[1]를 result에 덧붙인다.

   5. glue와 this[2]를 result에 덧붙인다. 

   6. this.length개의 항목이 모두 추가될 때까지 이 일을 반복한다.

   7. result를 반환한다. 

 

◾️ 기존에 call을 사용했던 방식처럼 this를 받고, this[0], this[1] 등이 합쳐진다. 이렇게 내부 알고리즘이 구현되어있기 때문에 어떤 유사 배열이던 this가 될 수 있다. 

 

 

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

◾️함수 또는 메서드를 데코레이터로 감싸 대체하는 것은 대체적으로 안전하다. 

◾️ 그런데 원본 함수에 func.calledCount 등의 프로퍼티가 있으면 데코레이터를 적용한 함수에선 프로퍼티를 사용할 수 없으므로 안전하지 않다. 함수에 프로퍼티가 있는 경우엔 데코레이터 사용에 주의해야 한다. 

◾️ 예시에서 함수 slow에 프로퍼티가 있었다면, cachingDecorator(slow) 호출 결과인 래퍼엔 프로퍼티가 없다. 

◾️ 몇몇 데코레이터는 자신만이 프로퍼티를 갖기도 한다. 데코레이터는 함수가 얼마나 많이 호출되었는지 세거나 호출 시 얼마나 많은 시간이 소모되었는지 등의 정보를 래퍼의 프로퍼티에 저장할 수 있다. 

◾️ 함수 프로퍼티에 접근할 수 있게 해주는 데코레이터를 만드는 방법도 있다. → Proxy 사용

 

📝 요약

◽️ 데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킨다. 주요 작업은 여전히 함수에서 처리한다.
◽️ 데코레이터는 함수에 추가된 기능정도로 보면 된다. 하나 혹은 여러 개의 데코레이터를 추가해도 함수의 코드는 변경되지 않는다. 
◽️ func.call(context, arg1, arg2...) - 주어진 컨텍스트와 인수를 사용해 func를 호출한다. 
◽️ func.apply(context, args) - this에 context가 할당되고, 유사 배열 args가 인수로 전달되어 func가 호출된다.
◽️ 콜 포워딩(call forwarding)은 대개 apply를 사용해 구현한다. 

◽️ 메서드 빌리기 : 특정 객체에서 메서드를 가져오고, 다른 객체를 컨텍스트로 고정한 후 함수를 호출(call)하는 형태
◽️ 메서드 빌리기는 배열 메서드를 빌려서 이를 arguments에 적용할 때 흔히 사용된다. 
◽️ 나머지 매개변수와 배열을 함께 사용하면 유사한 기능을 구현할 수 있다. 

 

 

 



📝 오늘 새롭게 배운 내용 📝

 

✅ 너무 새로운 내용이다. 

✅ 다시 보고, 과제까지 풀어봐야할 거 같다. 

 

 

 

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

 

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

 

ko.javascript.info

 

📌 [MDN call()]

 

Function.prototype.call()

call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.

developer.mozilla.org

📌 [MDN apply()]

 

Function.prototype.apply()

apply() 메서드는 주어진 this 값과 배열 (또는 유사 배열 객체) 로 제공되는 arguments 로 함수를 호출합니다.

developer.mozilla.org