수정할 내용을 '반영해주는' '함수' 이다.
자세한 내용은 생략하고 데코레이터가 뭔지 직관적으로 이해할 수 있도록 그림으로 그려보았다.
그림에서도 알 수 있듯이, 데코레이터는 무언가를 받아서 그것을 토대로
추가/수정 하여 내뱉는 '함수' 이다.
새로운 것을 만들어내는 것이 아니라, 기존에 존재하던 '원래 것' 을 기반으로 만든다.
별거 없다. 함수인데, 대상 위에다가 함수이름만 갖다 붙여도 반영이 되는 편의성을 지녔을 뿐이다.
자세히 공부하기전에 머리에 박아넣을 배경설정 :
@decrorator
형태로 붙는다.target
: 대상 클래스의 생성자 함수function classDecorator<T extends { new (...args: any[]): {} }>(target: T) {
return class extends target {
constructor(...args: any[]) {
super(args);
}
public print() {
console.log('This is decorated print.');
}
};
}
@classDecorator
class originalClass {
print() {
'This is original print.';
}
}
new originalClass().print();
This is decorated print.
function classDecoratorFactory(arg: string) {
return function <T extends { new (...args: any[]): {} }>(target: T) {
return class extends target {
constructor(...args: any[]) {
super(args);
}
public print() {
console.log('This is decorted print method with', arg);
}
};
};
}
@classDecoratorFactory('argument')
class originalClass2 {
public print() {
console.log('This is original print.');
}
}
new originalClass2().print();
This is decorted print method with argument
classDecoratorFactory
의 반환되는 값임을 확인 할 수가 있다.classDecoratorFactory
로 받고, 그 인수들을 활용한 함수를 return 값으로 반환하는 방식이다.argu
를 전달받아 originalClass2
의 오버라이딩 된 print()
메서드에 전달하여 사용하는 형태이다.function ClassDecorator(decorateMsg: string) {
return function (constructor: typeof originalClass) {
const originalFunc = constructor.prototype.print;
constructor.prototype.print = function (message: string) {
originalFunc(message);
console.log(decorateMsg);
};
};
}
@ClassDecorator('Decorater added this message.')
class originalClass {
print(message: string): void {
console.log(message);
}
}
new originalClass().print('User write this message.');
User write this message.
Decorater added this message.
print()
를 originalMethod
에 저장하고, constructor.prototype.print
에 기존내용과 덧붙인 내용을 활용하여 재 정의했다.target
: 메서드가 포함되어 있는 클래스methodName
: 메서드의 이름descriptor
: 메서드의 프로퍼티 설정function methodDecorator(
target: any,
methodName: string,
descriptor: PropertyDescriptor,
) {
console.log('target', target);
console.log('methodName', methodName);
console.log('descriptor', descriptor);
}
class decorateTestClass {
@methodDecorator
public originalFunction() {}
}
target {}
methodName originalFunction
descriptor {
value: [Function: originalFunction],
writable: true,
enumerable: false,
configurable: true
}
property attribute
를 기본값으로 자동 정의한다.PropertyDescriptor
는 property attribute
가 명시된 객체이다.descriptor
의 속성을 바꿔보았다.function methodDecoratorFactory(canBeEdit: boolean = false) {
return function (
target: any,
methodName: string,
descriptor: PropertyDescriptor,
) {
descriptor.writable = canBeEdit;
};
}
class decoratorTestClass {
@methodDecoratorFactory()
first() {
console.log('first original');
}
@methodDecoratorFactory(true)
second() {
console.log('second original');
}
@methodDecoratorFactory(false)
third() {
console.log('third original');
}
}
const decoratorTestInstance = new decoratorTestClass();
// runtime error
decoratorTestInstance.first = function () {
console.log('first new');
};
decoratorTestInstance.second = function () {
console.log('second new');
};
// runtime error
decoratorTestInstance.third = function () {
console.log('third new');
};
decoratorTestInstance.first();
decoratorTestInstance.second();
decoratorTestInstance.third();
first original
second new
third original
second
만 수정된 문자열이 출력되고, 그 외에는 수정되지 못했다.class decoratorTestClass2 {
@LogError('Input here error message.')
originalMethod(originalPrintMessage: string): void {
console.log(originalPrintMessage);
throw new Error();
}
}
function LogError(errorMessage: string) {
return function (
target: any,
methodName: string,
descriptor: PropertyDescriptor,
): void {
const originalFunc = descriptor.value;
descriptor.value = function (e: string) {
try {
originalFunc(e);
} catch (err) {
console.log(errorMessage);
}
};
};
}
new decoratorTestClass2().originalMethod('This original message log.');
This original message log.
Input here error message.
descriptor.value
는 대상 메서드 그 자체 이다.target
: 프로퍼티가 포함되어 있는 클래스propertyName
: 프로퍼티의 이름내용 보강 예정
Reflect.getMetadata
, "design:type"
function logType(target : any, key : string) {
var t = Reflect.getMetadata("design:type", target, key);
console.log(`${key} type: ${t.name}`);
}
class Demo{
@logType // apply property decorator
public attr1 : string;
}
attr1 type: String
function propertyDecorator(target: any, propName: string): any {
console.log(target);
console.log(propName);
return {
writable: false
};
}
class TestPropertyDecorator {
@propertyDecorator
doChangeThis: string = 'Before';
}
const decoratorTestInstance = new TestPropertyDecorator();
decoratorTestInstance.doChangeThis = 'After';
{}
doChangeThis
[runtime error]
doChangeThis: string = 'Before';
^
TypeError: Cannot assign to read only property 'doChangeThis' of object '#<TestPropertyDecorator>'
위와 같이 discriptor 설정을 수정하여, 프로퍼티의 변경을 방지할 수도 있다.
const allowlist = ["Jon", "Jane"];
// 프로퍼티를 변경할 때, 허용된 값만 반영 시키는 함수
const allowlistOnly = (target: any, memberName: string) => {
let currentValue: any = target[memberName];
Object.defineProperty(target, memberName, {
set: (newValue: any) => {
if (!allowlist.includes(newValue)) {
return;
}
currentValue = newValue;
},
get: () => currentValue
});
};
class Person {
@allowlistOnly
name: string = "Jon";
}
const person = new Person();
console.log(person.name);
person.name = "Peter";
console.log(person.name);
person.name = "Jane";
console.log(person.name);
//Output
Jon
Jon
Jane
// 2-3-2 예제에서, 이번에는 허용 할 값들을 배열로 받아서 반영시키기
const allowlistOnly = (allowlist: string[]) => {
return (target: any, memberName: string) => {
let currentValue: any = target[memberName];
Object.defineProperty(target, memberName, {
set: (newValue: any) => {
if (!allowlist.includes(newValue)) {
return;
}
currentValue = newValue;
},
get: () => currentValue
});
};
}
class Person {
@allowlistOnly(["Claire", "Oliver"])
name: string = "Claire";
}
const person = new Person();
console.log(person.name);
person.name = "Peter";
console.log(person.name);
person.name = "Oliver";
console.log(person.name);
//Output
Claire
Claire
Oliver
미해결 질문
- 프로퍼티 데코레이터의 리턴은 property descriptor 형태이다 ?
- descriptor.value 는 왜 메소드를 가리키는가?
- meta-data를 사용해야 할 수 밖에 없는 이유는 무엇인가?
공부해야할 주제
- metadata-reflection
- Nominal typing VS Structural typing(Duck typing)
참고한 링크
- https://www.typescriptlang.org/ko/docs/handbook/decorators.html
- https://www.digitalocean.com/community/tutorials/how-to-use-decorators-in-typescript
- https://www.youtube.com/watch?v=favycnvMY1Q
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-4
- https://dparkjm.com/typescript-decorators
- https://wonism.github.io/what-is-decorator