[JavaScript] Decorator

mechaniccoder·2020년 11월 28일
0

decorator?

데코레이터는 함수를 인자로 받는 함수입니다. 인자로 받은 함수의 로직에 기능을 추가하거나 행동을 변화시키는 역할을 하는 것이 바로 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 으로 복사됩니다. 그런데 functhis 값은 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 메서드를 따로 설명하지는 않겠습니다. 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 즉, workerslow 메서드의 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가 출력됩니다.

정리

오늘은 데코레이터에 대해서 알아봤습니다. 평소에 두루뭉실하게만 알았지, 정확한 쓰임새와 개념에 대해서 알지 못했는데 이번 기회를 통해 정확하게 알게 돼서 한 걸음 더 성장한 것 같습니다.

profile
세계 최고 수준을 향해 달려가는 개발자입니다.

0개의 댓글