함수를 인자로 받고, 그 함수의 행동을 변경시켜서 반환하는 함수를 데코레이터(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() 안에 캐싱 관련 코드를 추가하는 대신, 래퍼 함수를 만들어서 캐싱 기능을 추가하고 있다.
위에서 구현한 캐싱 데코레이터는 객체 메서드에 사용하기에 적합하지 않다.
// 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;
};
}
이번에는 인자가 여러개인 함수를 캐싱처리 해야하는 상황이라고 가정하면, 여러개의 인자를 하나의 키 값으로 만들어주는 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을, 유사 배열 형태라면 apply를 사용하면 된다.
이렇게 컨텍스트와 함께 인수 전체를 다른 함수에 전달하는 것을 콜 포워딩(call forwarding) 이라고 부른다.
참고자료