데코레이터는 함수를 인자로 받는 함수입니다. 인자로 받은 함수의 로직에 기능을 추가하거나 행동을 변화시키는 역할을 하는 것이 바로 decorator
입니다.
백문이 불여일견이겠죠. 바로 코드예제를 봅시다.
function slow(x) {
return x;
}
// 데코레이터 함수
function cachingDecorator(func) {
const cache = new Map(); // 객체를 키로 사용하기 위해 Map 자료구조를 활용합니다.
return function(x) { // decorator로부터 반환되는 함수를 wrapper라고 부릅니다.
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x);
cache.set(x, result);
return result;
}
}
slow = cachingDecorator(slow);
console.log(slow(1));
console.log('cache', slow(1)); // cache에 저장된 값으로 불러옵니다.
console.log(slow(2));
console.log('cache', slow(2)); // cache에 저장된 값으로 불러옵니다.
데코레이터로부터 반환된 함수를 래퍼(wrapper)
라고 하며 이 함수는 기존의 slow함수의 로직에 cache로직을 추가합니다. slow함수의 로직을 건드리지 않고 분리되어 있기 때문에 복잡하지도 않습니다.
실행컨텍스트와 클로저에 익숙하신 분들은 여기서 그 개념이 사용된 것을 알 수 있습니다.
래퍼함수가 클로저 함수로 사용됐고 cache
자유변수를 참조하고 있기 때문에 데코레이터의 변수객체가 가비지컬렉터에 정리되지 않는것을 알 수 있습니다. ✨
그런데 만약 메서드를 데코레이팅한다면 문제가 발생합니다.
const worker = {
someMethod() {
return 1;
},
slow(x) {
return x * this.someMethod();
}
}
function cachingDecorator(func) {
const cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x);
cache.set(x, result);
return result;
}
}
worker.slow = cachingDecorator(worker.slow); // 이걸 이해하는게 중요합니다.
console.log(worker.slow(2));
cachingDecorator(worker.slow)
데코레이터 인수로 메서드를 전달하고 있고, 전달된 메서드는 함수 내부에서 func
으로 복사됩니다. 그런데 func
의 this
값은 undefined입니다. 이를 이해하기 위해서 다음 예제를 테스트해보죠.
example 1
const obj = {
name: 'mechaniccoder',
sayHi() {
console.log(`Hi!, ${this.name}`);
}
}
obj.sayHi(); // Hi!, mechaniccoder
// 잘 작동하죠?
example 2
const obj = {
name: 'mechaniccoder',
sayHi() {
console.log(`Hi!, ${this.name}`);
}
}
const func = obj.sayHi;
func(); // Hi!, undefined
// 이건 잘 작동하지 않습니다.
이 예제를 설명하기 앞서 this를 이해하는 것이 중요할 것 같습니다. 제가 이해한 this는 런타임에서 함수를 호출하는 주체입니다. 보통 .
앞에 사용된 객체를 나타내죠.
따라서, 첫번째 예제의 경우 .
앞에 obj가 명시되어있으므로 this=obj
이지만, 두번째 예제에서는 func()
으로 호출했기 때문에 .
이 없으므로 this=undefined
이 됩니다.
자, 그럼 데코레이터 메서드 예제에서 왜 this가 undefined값을 할당받는지 이해가 됐죠? 그렇다면 이를 어떻게 해결할 수 있을까요? 🤔
바로 함수의 내장메서드 call
을 활용하면 됩니다.
call 메서드를 따로 설명하지는 않겠습니다. call, apply, bind 함수의 차이를 공부하면 많은 도움이 될 겁니다.
바로 call 메서드를 사용해서 this값을 바인딩해보겠습니다.
const worker = {
someMethod() {
return 1;
},
slow(x) {
return x * this.someMethod();
}
}
function cachingDecorator(func) {
const cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // 여기 이렇게 call메서드를 사용했습니다.
cache.set(x, result);
return result;
}
}
worker.slow = cachingDecorator(worker.slow); --- (1)
console.log(worker.slow(2)); // 정상적으로 2가 출력됩니다.
(1) 코드를 보시면 worker.slow
는 래퍼함수가 됩니다. 즉, 래퍼함수의 this
값은 .
앞에 있는 worker
가 되겠죠? 따라서 래퍼 함수 내부에서 this
즉, worker
를 slow
메서드의 this
에 바인딩시켜준겁니다.
위의 예제에서는 slow(x)로 인수를 하나만 가집니다. 그런데 만약 인수가 복수라면 어떻게 캐싱해야 될까요? 해싱을 활용하면 됩니다. 예제를 살펴보죠.
const worker = {
someMethod() {
return 1;
},
slow(x, y) {
return x * y * this.someMethod();
}
}
function cachingDecorator(func) {
const cache = new Map();
return function(x) {
let key = hash(arguments);
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // spread operator를 활용해 여러 인수를 보냅시다.
cache.set(key, result);
return result;
}
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow); --- (1)
console.log(worker.slow(2)); // 정상적으로 2가 출력됩니다.
오늘은 데코레이터에 대해서 알아봤습니다. 평소에 두루뭉실하게만 알았지, 정확한 쓰임새와 개념에 대해서 알지 못했는데 이번 기회를 통해 정확하게 알게 돼서 한 걸음 더 성장한 것 같습니다.