call/apply와 데코레이터, 포워딩
1. 데코레이터
함수를 인자로 받고, 그 함수의 행동을 변경시켜서 반환하는 함수를 데코레이터(decorator) 라고 한다.
function slow(x) {
// many operations
return x;
}
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;
}
}
slow = cachingDecorator(slow);
연산이 매우 많지만 안정적인(input과 ouput이 1대1) 함수 slow(x)
가 있다고 가정했을 떄, slow()
안에 캐싱 관련 코드를 추가하는 대신, 래퍼 함수를 만들어서 캐싱 기능을 추가하고 있다.
- 모든 함수를 대상으로
cachingDecorartor
를 적용할 수 있기 때문에 유용하다. - 캐싱 관련 코드를 함수로 분리할 수 있어
slow
자체의 복잡성이 증가하지 않는다. - Decorator 적용 전과 후의 결과가 동일하다.
2. '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.call(context, arg1, arg2, ...);
위의 내장 함수 메서드를 아래와 같이 호출하면 거의 동일한 일이 발생한다.
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;
};
}
3. 'call.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);
이렇게 컨텍스트와 함께 인수 전체를 다른 함수에 전달하는 것을 콜 포워딩(call forwarding) 이라고 부른다.
요약
- 데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킨다. 주요 작업은 여전히 함수에서 처리한다.
- func.call(context, arg1, arg2, ...) - 주어진 컨텍스트와 인수를 사용해 func 를 호출한다.
- func.apply(context, args) - this에 context가 할당되고, 유사 배열 args가 인수로 전달되어 func이 호출된다.
- 콜 포워딩은 보통 apply를 사용해서 구현한다.
댓글