...
타입스크립트 Decorator 란
타입스크립트로 Angular.js 나 Nest.js 개발을 진행하다보면 필연적으로 데코레이터란 것을 접하게 된다.
자바(Java)를 해본 분들이라면 어노테이션과 비슷하다고 볼수 있지만, 자바 어노테이션은 컴파일 타임에 상관지만 타입스크립트 데코레이터는 컴파일 타임에는 상관하지 않아, 오히려 파이썬(Python)의 데코레이터와 거의 비슷하다고 말하는게 옳다.
데코레이터는 일종의 함수 이다.
데코레이터는 말 그대로 코드 조각을 장식해주는 역할을 하며, 타입스크립트에서는 그 기능을 함수로 구현하는 것이다.
예를들어 메소드 / 클래스 / 프로퍼티 / 파라미터 위에 @함수 를 장식해줌으로써, 코드가 실행(런타임)이 되면 데코레이터 함수가 실행되어, 장식한 멤버를 보다 파워풀하게 꾸며주는 것으로 이해하면 된다.
@(at) 기호는 타입스크립트에게 이것이 데코레이터임을 알려주고, 타입스크립트는 클래스 실행 시 플러그인 형태로 실행되게 해준다.
function readonly(writable: boolean) {
return function (target: any, decoratedPropertyName: any): any {
return {
writable: !writable,
};
};
}
class Test {
property = 'property';
@readonly(false)
public data1 = 0;
@readonly(true)
public data2 = 0;
}
const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.
- 먼저 클래스 프로퍼티의 외부 접근(writable) 설정을 위한 데코레이터를 달고 싶은 곳에
@함수를 위에다 명시 - 소스 코드를 실행하게 되면, 위에 장식된 데코레이터 표현식이 런타임에 함수로서 호출됨
- 데코레이터 함수에서 객체의 writable 속성을 설정하는 로직을 모두 실행하면, 장식을 한 주체 프로퍼티에 속성이 적용됨.
- 따라서 data2 프로퍼티는 쓰기가 불가능하여 런타임 에러가 일어남
이처럼, 데코레이터 패턴은 클래스를 수정하지 않고 클래스의 멤버들의 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴의 하나이다. 데코레이터 패턴을 사용하면 전체 기능에 신경 쓰지 않고 특정 인스턴스에 초점을 맞출 수 있다.
지금까지의 데코레이터(decorator) 특징을 열거하면 다음과 같다.
- 데코레이터는 클래스 선언, 메서드(method), 접근자(accessor), 프로퍼티(property) 또는 매개변수(parameter)에 첨부할 수 있는 특수한 종류의 선언
- 데코레이터 함수에는 target(현재타겟), key(속성이름), descriptor(설명)가 전달 (단, 어떤 멤버를 장식했느냐에 따라 인수가 달라짐)
- 메소드나 클래스 인스턴스가 만들어지는 런타임에 실행. 즉, 매번 실행되지 않음.
- 데코레이터는 클래스 또는 클래스 내부의 생성자, 프로퍼티 , 접근자, 메서드, 그리고 매개변수에만 장식될 수 있음
데코레이터 설정
타입스크립트의 데코레이터는 정식 지원하는 문법 기능이 아니고, 실험적인(experimental) 기능으로 빠져 있다.
이렇게 보면 잘 안쓰이는가 싶겠지만, 특정 프레임워크(앙귤러, nest)에선 정말 주구장창 쓰이기에 아마 이전 버전 호환성 때문에 옵션으로 놔둔게 아닐까 싶다.
어쨋든 타입스크립트에서 데코레티어를 사용하기 위해선 tsconfig.json 파일에서 experimentalDecorators 옵션을 활성화 해야 한다.
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
}
}
데코레이터 팩토리
데코레이터 팩토리 함수는 데코레이터 함수를 감싸는 래퍼 함수 이다. 보통 데코레이터가 선언에 적용되는 방식을 원하는 대로 바꾸고 싶을때 사용된다.
프로그래밍에서 함수에게 사용자가 인자를 전달할 수 있는 것과 유사하게, 데코레이터 함수 또한 팩토리를 사용해 사용자로부터 인자를 전달 받도록 설정할 수 있다.
즉, 데코레이터 팩토리는 사용자로부터 전달 받은 인자를, 내부에서 반환되는 데코레이터 함수는 데코레이터로 사용되는 것이다.
일반적으로 데코레이터에서는 팩토리를 만들어 사용하고 있기 때문에 거의 디폴트처럼 사용된다고 보면 된다.
아래 코드를 보면, 클로저 변수로 사용자 인자를 받고 리턴값으로 데코레이터 함수를 반환하여 데코레이터를 실행하는 것을 볼 수 있다.
// 데코레이터 팩토리
function Component(value: string) {
// 데코레이터 함수
return function (target: Function) {
console.log(value);
console.log(target);
};
}
// 데코레이터 팩토리를 사용하면 인자 값을 전달할 수 있다.
@Component('안녕하세요 !!')
class TabsComponent {}
const tabs = new TabsComponent();
[Running] ts-node "index.ts"
'안녕하세요 !!'
[class TabsComponent]
멀티 데코레이터
데코레이터의 또 다른 장점으로는 하나의 멤버에 동시에 여러 개의 데코레이터를 장식할 수 있는 점이다.
이를 데코레이터 합성 (Decorator Composition) 이라고도 부르기도 한다.
멀티 데코레이터의 실행 흐름은 다음 순으로 처리된다.
- 각 데코레이터 표현식은 위에서 아래 방향(⬇︎)으로 평가하고
- 실행 결과는 아래에서 위로(⬆︎) 함수를 호출
굉장히 난해한 개념인데, 데코레이터 팩토리를 사용해 멀티 데코레이터의 실행 흐름을 살펴보자.
// Size 데코레이터 팩토리
function Size() {
console.log('Size(): 평가됨');
// Size 데코레이터
return function (target: any, prop: string, desc: PropertyDescriptor) {
console.log('Size(): 실행됨');
};
}
// Color 데코레이터 팩토리
function Color() {
console.log('Color(): 평가됨');
// Color 데코레이터
return function (target: any, prop: string, desc: PropertyDescriptor) {
console.log('Color(): 실행됨');
};
}
// Button 클래스 정의
class Button {
// 메서드에 멀티 데코레이터 적용
@Size()
@Color()
isPressed() {}
}
[Running] ts-node "index.ts"
Size(): 평가됨
Color(): 평가됨
Color(): 실행됨
Size(): 실행됨
@expression 에서 expression 표현식을 함수로 평가하는 순서는 위에서 아래 이다.
그리고 expression 이 함수로 평가된(데코레이터 팩토리) 후에, 실행 결과(데코레이터 함수)가 실행되는 순서는 아래에서 위 된다.
Decorator 종류
데코레이터는 클래스, 메서드, 프로퍼티, 접근자, 파라미터의 선언에만 장식 될 수 있다.
그리고 데코레이터가 어디에 장식되느냐에 따라서, 데코레이터 함수로 넘어오는 인자의 길이나 리턴 구성이 달라지게 된다.
Class Decorator
클래스 데코레이터는 클래스 선언 직전에 선언된다.
기존의 클래스 정의를 확장하는 용도로 사용할 수 있다.
클래스 데코레이터 매개변수로는 클래스 생성자 자체를 받게 되는 특징이 있다.
- 클래스 데코레이터 매개변수
- 첫 번째 argument (constructor) : 클래스(생성자 함수)가 전달타입스크립트가 클래스를 실행할 때 클래스의 생성자를 데코레이터의 constructor 파라미터로 자동 전달하므로, 생정자를 명시적으로 전달하지 않아도 된다.
- 클래스 데코레이터 리턴값
- class, void
function classDecorator<T extends { new (...args: any[]): {} }>(constructorFn: T) // 제네릭 - 해당 함수를 호출할 때 동적으로 <>에 정의된 타입을 받음. 여기선 클래스 생성자 타입으로 받는의미
{
...
} // 리턴값은 class 혹은 void
아래 코드를 보면, 데코레이터가 Test의 constructor를 상속해서 추가로 프로퍼티를 지정하고 리턴해주었는데, 이게 그대로 Test 클래스의 constructor 쪽으로 가서 확장이 된다고 생각하면 된다.
그래서 실행 결과가 기존의 Test의 생성자에 없던 프로퍼티들이 추가된 것이다.
데코레이터가 클래스의 멤버를 확장한 것이다!
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
// Test 클래스의 constructor를 상속해서 new Test() 가 되면 추가된 생성자도 실행되도록 설정
return class extends constructor {
first_prop = 'override'; // 데코레이터에서 새로 프로퍼티를 덮어씌움
new_prop = 'new property'; // 데코레이터에서 새로 프로퍼티를 추가
};
}
@classDecorator
class Test {
first_prop: string;
constructor(m: string) {
this.first_prop = m;
}
}
let t = new Test('abcdefg');
console.log(t);
console.log( t.first_prop ); // 'override'
// 데코레이터로 설정된 프로토타입 확장은 타입 단언(Type Assertion) 필요
console.log( (t as any).new_prop ); 'new property'
[Running] ts-node "index.ts"
Test {
first_prop: 'override',
new_prop: 'new property'
}
override
new property
이번엔, 데코레이터 컨테이너를 이용한 예제이다.
데코레이터 컨테이너에 전달된 사용자 인자값을 데코레이터 함수에서 사용해 결과를 리턴한다.
// 데코레이터 컨테이너
function classDecorator(param1: string, param2: string) {
// 데코레이터 함수
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
new_prop = param1;
first_prop = param2;
};
};
}
@classDecorator('안녕하세요', '반갑습니다')
class Test {
first_prop: string;
constructor(m: string) {
this.first_prop = m;
}
}
let t = new Test('world');
console.log(t);
[Running] ts-node "index.ts"
Test {
first_prop: '반갑습니다',
new_prop: '안녕하세요'
}
지금 까지 장식된 클래스의 constructor를 확장했지만, 프로토타입을 이용하여 확장할 수 있다.
function classDecoratorFactory<T extends { new (...args: any[]): {} }>(constructorFn: T) {
// 프로토타입으로 새로운 프로퍼티를 추가
constructorFn.prototype.print2 = function () {
console.log('this is print2');
};
constructorFn.prototype.gender = 'male';
// 클래스를 프로퍼티에 상속시켜 새로운 멤버를 추가 설정
return class extends constructorFn {
public name = 'mark';
private _age = 36;
constructor(...args: any[]) {
super(args);
}
public print() {
console.log('this is print');
}
};
}
@classDecoratorFactory
class Test2 {}
const test2 = new Test2();
console.log(test2); // class_1 { name: 'mark', _age: 36 }
// 클래스 Test의 타입에는 print 함수가 없고, 데코레이터로 동적으로 추가된 형태이니, 타입 단언을 사용
(test2 as any).print(); // this is print
(test2 as any).print2(); // this is print2
console.log((test2 as any).gender); // male
[Running] ts-node "index.ts"
Test2 { name: 'mark', _age: 36 }
this is print
this is print2
male
Method decorator
메서드 데코레이터는 메서드 선언 직전에 선언 된다. 메서드 관찰, 수정 또는 대체하는 데 사용할 수 있다.
가장 많이 이용되는 데코레이터가 메서드 데코레이터 이다.
클래스 데코레이터는 클래스(생성자 함수)를 extends 하는 방법으로 기능을 확장할 수 있었지만, 메서드 데코레이터는 메서드의 Property Descriptor를 수정하여 메서드를 확장한다.
Property Descriptor는 객체의 프로퍼티들을 기존보다 정교하게 정의할 수 있는 ES5의 스펙이다.
이 Property Descriptor는 Object.getOwnPropertyDescriptor 를 사용해서 가져올 수 있다.
메서드 데코레이터 함수가 전달 받는 인자는 총 3가지로 다음과 같다.
- 메서드 데코레이터 매개변수
- 첫 번째 argument : static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
- 두 번째 argument : 해당 method의 이름
- 세 번째 argument : 해당 method의 property descriptor
function methodDecorator(
target: any, // static 메서드라면 클래스의 생성자 함수, 인스턴스의 메서드라면 클래스의 prototype 객체
propertyKey: string, // 메서드 이름
descriptor: PropertyDescriptor // 메서드의 Property Descriptor
) {
...
} // return 값 무시됨
메서드 데코레이터는 클래스, 메서드 이름, 호출된 메서드(해당 메서드는 Object.defineProperty()로 만들어 짐)로 정의된 메서드 순으로 전달받는다. 만약 해당 메서드가 호출될 때 특정 무엇인가를 동작시키고 싶다면 데코레이터에서 descriptor를 다음과 같이 재정의 해주면 된다.
descriptor 속성 | Description |
value | 현재 값 value |
writable | 수정 가능하면 true, 아니면 false |
enumarable | |
configurable | Property definition이 수정 및 삭제가 가능하면 true, 아니면 false |
function methodDecorator() {
return function (target: any, property: string, descriptor: PropertyDescriptor) {
// descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
let originMethod = descriptor.value;
// 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
descriptor.value = function (...args: any) {
console.log('before');
originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call,apply,bind 를 통해 호출
console.log('after');
};
};
}
class Test {
property = 'property';
hello: string;
constructor(m: string) {
this.hello = m;
}
@methodDecorator()
test() {
console.log('test');
}
}
let test = new Test("world")
test.test()
[Running] ts-node "index.ts"
before
test
after
로직은 약간 난해하지만 그렇게 어렵지 않다.
데코레이터 인수의 descriptor.value 로 접근하면 장식된 함수 자체로 접근하게 되는데 이를 잠시 백업하고 나중에 실행하는 식으로 응용이 가능하다.
중요한 점은 백업된 메서드를 호출할 때 apply를 이용하여 this를 바인딩해줘야 한다는 점이다. 만일 this를 바인딩하지 않으면 해당 메서드를 어느 객체가 호출 했는지 알 수 없기 때문이다.
여기서 this는 메서드를 호출하는 객체를 의미한다.
좀더 응용하자면, 메서드 데코레이터로 특정 메서드의 실행 전/후 로깅, 실행시간을 측정 가능하다.
function methodDecorator() {
return function (target: any, property: string, descriptor: PropertyDescriptor) {
// descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
let originMethod = descriptor.value;
// 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
descriptor.value = function (...args: any) {
let startTS = new Date().getTime();
originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call,apply,bind 를 통해 호출
let endTS = new Date().getTime();
console.log(`실행시간: ${(endTS - startTS) / 1000} S`);
};
};
}
// ...
[Running] ts-node "index.ts"
test
실행시간: 0 S
Property Decorators
프로퍼티 데코레이터는 프로퍼티 선언 바로 전에 선언 된다.
프로퍼티 데코레이터는 메서드 데코레이터와 다르게 데코레이터 함수에 Property Descriptor 가 인자로서 제공되지 않는다는 차이가 있다.
대신에 프로퍼티 데코레이터도 마찬가지로 Property Descriptor 형식의 객체를 반환해서 프로퍼티의 설정을 바꿀 수 있다.
Property Descriptor 가 인자로서 제공되지 않으니, 프로퍼티 데코레이터는 매개변수가 두개이다.
- 프로퍼티 데코레이터 매개변수
- 첫 번째 argument : static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
- 두 번째 argument : 해당 property의 이름
- 프로퍼티 데코레이터 리턴값
- Property Descriptor 형태
- void
function propertyDecorator(
target: any, // static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
propName: string, // 프로퍼티 이름
) {
...
} // return하는 값이 Property Descriptor 형태임. 혹은 void. 이로서 해당 property의 writable 등을 조작할 수 있음
리턴값으로 Object.defineProperty()를 반환함으로써, 이를 통해 속성의 부가적인 설정을 할 수 있다.
예를들어 다음과 같이, 앞의 데코레이터는 boolean 타입의 인자를 전달받아 writable로 설정하게 되는데 writable이 false일 경우 해당 속성은 값을 수정할 수 없게 된다.
function writable(writable: boolean) {
return function (target: any, decoratedPropertyName: any): any {
return {
writable,
};
};
}
class Test {
property = 'property';
@writable(false)
public data1 = 0;
@writable(true)
public data2 = 0;
}
const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.
좀더 응용하자면 다음과 같이 getter와 setter도 설정이 가능하다.
function SetDefaultValue(numberA: number, numberB: number) {
return (target: any, propertyKey: string) => {
const addNumber = numberA * numberB;
let value = 0;
// 데코레이터가 장식된 DataDefaultType의 num 이라는 프로퍼티의 객체 getter / setter 설정을 추가한다.
Object.defineProperty(target, propertyKey, {
get() {
return value + addNumber; // 조회 할때는 더하기 시킴
},
set(newValue: any) {
value = newValue - 30; // 설정 할때는 30을 뺌
},
});
};
}
class DataDefaultType {
@SetDefaultValue(10, 20)
num: number = 0;
}
const test = new DataDefaultType();
test.num = 30;
console.log(`num is 30, 결과 : ${test.num}`); // num is 30, 결과 : 200
test.num = 130;
console.log(`num is 130, 결과 : ${test.num}`); // num is 130, 결과 : 300
[Running] ts-node "index.ts"
num is 30, 결과 : 200
num is 130, 결과 : 300
Parameter Decorators
매개변수 데코레이터는 매개 변수 선언 직전에 선언 된다.
함수의 매개변수 왼쪽 옆에 명시해 주면 된다. 사실 데코레이터는 가로로 써도 인식되기 때문이다.
파라미터 데코레이터 함수가 전달 받는 인자는 총 3가지로 다음과 같다.
- 파라미터 데코레이터 매개변수
- 첫 번째 argument : static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
- 두 번째 argument : 매개변수가 들어있는 method의 이름
- 세 번째 argument : 메서드 파라미터 목록에서의 index
function parameterDecorator(
target: any, // static 메서드의 파라미터 데코레이터라면 클래스의 생성자 함수, 인스턴스의 메서드라면 prototype 객체
methodName: string, // 매개변수가 들어있는 메서드의 이름
paramIndex: number // 매개변수의 순서 인덱스
) {
...
} // 리턴값은 무시됨
파라미터 데코레이터 구현 코드 예제는 다음과 같다.
function parameterDecorator(target: any, methodName: string, paramIndex: number) {
console.log('parameterDecorator start');
console.log(target);
console.log(methodName);
console.log(paramIndex);
console.log('parameterDecorator end');
}
class Test7 {
private _name: string;
private _age: number;
constructor(name: string, @parameterDecorator age: number) {
this._name = name;
this._age = age;
}
print(@parameterDecorator message: string) {
console.log(message);
}
}
[Running] ts-node "index.ts"
parameterDecorator start
Test7 { print: [Function] }
print
0
parameterDecorator end
parameterDecorator start
[Function: Test7]
undefined
1
parameterDecorator end
데코레이터 호출 순서
마지막으로 데코레이터 호출 순서를 알아보도록 하자.
호출 순서는 property → method → parameter → class 순서로 되는걸 볼 수 있다.
function classDecorator() {
console.log('class');
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {};
};
}
function methodDecorator() {
console.log('method');
return function (target: any, property: any, descriptor: any) {};
}
function propertyDecorator(writable: boolean = true) {
console.log('property');
return function (target: any, decoratedPropertyName: any): any {};
}
function parameterDecorator() {
console.log('parameter');
return function (target: any, methodName: string, paramIdx: number) {};
}
@classDecorator() // 4
class Test {
@propertyDecorator() // 1
property = 'property';
@methodDecorator() // 2
test(@parameterDecorator() param1: string) { // 3
console.log('test1');
}
}
[Running] ts-node "index.ts"
property
method
parameter
class
# 참고자료
https://blog.naver.com/pjt3591oo/222120496022
https://www.youtube.com/watch?v=favycnvMY1Q&t=1055s
https://dparkjm.com/typescript-decorators
https://typescript-kr.github.io/pages/decorators.html
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.