[ TS ] @Decorator 데코레이터 (feat. nestJS)

UI SEOK YU·2023년 1월 6일
0

TypeScript

목록 보기
1/4
post-thumbnail

1. 데코레이터 (decorator) 란?

1-1. 요약

수정할 내용을 '반영해주는' '함수' 이다.

1-2. 그림으로 이해하기


자세한 내용은 생략하고 데코레이터가 뭔지 직관적으로 이해할 수 있도록 그림으로 그려보았다.
그림에서도 알 수 있듯이, 데코레이터는 무언가를 받아서 그것을 토대로
추가/수정 하여 내뱉는 '함수'
이다.
새로운 것을 만들어내는 것이 아니라, 기존에 존재하던 '원래 것' 을 기반으로 만든다.

별거 없다. 함수인데, 대상 위에다가 함수이름만 갖다 붙여도 반영이 되는 편의성을 지녔을 뿐이다.

1-3. 자세히 알기 전에

자세히 공부하기전에 머리에 박아넣을 배경설정 :

  • 데코레이터는 함수다.
  • @decrorator 형태로 붙는다.
  • 데코레이터는 클래스 / 매서드 / 프로퍼티 / 매개변수 4가지 유형에만 적용된다.
  • 각각의 유형의 작동원리를 파악하여, 데코레이터를 커스텀 하여 사용할 수 있다.
  • 컴파일 타임에는 그 함수의 타입만 체크하고, 런타임에 평가 및 사용이 된다.
  • 클래스가 인스턴스로 만들어질 때가 아니라, 클래스가 정의될 때 최초 한 번만 적용된다.

2. 데코레이터 유형

  • 데코레이터는 클래스 / 매서드 / 프로퍼티 / 매개변수 4가지 유형에 적용된다.
  • 데코레이터들은 어디에 쓰이는지 따라 받는 매개변수의 갯수가 다르다.


2-1. 클래스 데코레이터

  • 1개의 매개변수를 받는다.
    - target : 대상 클래스의 생성자 함수


2-1-1. 기본형태

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.
  • 클래스 데코레이터는 기본적으로 데코레이터를 적용할 클래스의 contructor를 불러와 (코드에서는 target) 클래스를 상속받아 수정하고 수정한 자식 클래스를 반환한다.
  • 만약에 인자들을 전달받아서 클래스에 반영시켜야 한다면 어떻게 될까? 매개변수를 받는 칸이 필요할텐데 이것을 어떻게 표현할까?
  • 위 예시에서 매개변수로 인자를 받는 함수를 겉에 한 번 더 씌우면 된다. 그 예시는 2-2-1 과 같다.


2-1-2. 응용형태 (1)

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
  • 2-1-2 예시를 보면 데코레이터 함수 내에 2-1-1과 같은 형태가 classDecoratorFactory 의 반환되는 값임을 확인 할 수가 있다.
  • 외부에서 전달받아야 하는 인수는 classDecoratorFactory 로 받고, 그 인수들을 활용한 함수를 return 값으로 반환하는 방식이다.
  • 위 예시에서는 매개변수 argu를 전달받아 originalClass2의 오버라이딩 된 print() 메서드에 전달하여 사용하는 형태이다.


2-1-3. 응용형태 (2)

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.
  • 위와 같이 대상 클래스를 반환하지 않고, 클래스의 prototype을 직접 수정하여 사용할 수도 있다.
  • 기존의print()originalMethod 에 저장하고, constructor.prototype.print 에 기존내용과 덧붙인 내용을 활용하여 재 정의했다.


2-2. 메서드 데코레이터

  • 메서드 데코레이터도 결국 대상 메서드를 변형시켜서 반환해주는 형태이다.
  • 메서드 데코레이터는 3개의 인자를 받는다. (target, propertyKey, descriptor)
    - target : 메서드가 포함되어 있는 클래스
    - methodName : 메서드의 이름
    - descriptor : 메서드의 프로퍼티 설정

  • 작성자가 직접 저 인자들을 넣어주는게 아니고, 데코레이터가 적용되는 대상에 따라 TypeScript 컴파일러가 자동으로 이러한 인자들을 데코레이터에게 제공한다.


2-2-1. 기본형태

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를 기본값으로 자동 정의한다.
  • PropertyDescriptorproperty attribute가 명시된 객체이다.
  • 위 예시는 originalFunction에 methodDecorator 를 붙였을 때, 각 매개변수(target, propertyKey, descriptor) 에 어떤 값이 할당 되는지 확인하기 위한 것이다.


2-2-2. 응용형태 (1)

  • 2-2-1 에서 확인한 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 만 수정된 문자열이 출력되고, 그 외에는 수정되지 못했다.


2-2-3. 응용형태 (2)

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 는 대상 메서드 그 자체 이다.


2-3. 프로퍼티 데코레이터

  • 2개의 매개변수를 받는다.
    - target : 프로퍼티가 포함되어 있는 클래스
    - propertyName : 프로퍼티의 이름

2-3-0. Metadata-Reflection

2-3-0-1. 필요한 이유

내용 보강 예정

  • reflection 은 동일한 시스템 내에서 다른코드 또는 자신을 검사하는데 사용하는 개념이다.
  • 자바스크립트의 어플리케이션의 크기가 커지면 커질수록, 복잡성을 해결하기 위해 제어의 역전등과 같은 유형의 툴이 필요하게 되었다.
  • 하지만 JS 자체에 reflection 이 없기 때문에 툴을 구현하려해도 제대로 구현되지 않는다는 문제가 있었다.
  • reflection API는 런타임에 정보가 필요한 해당 데이터의 데이터를(이것이 meta-data) 찾을 수 있어야 한다.
  • JS 에도 객체의 정보를 찾기위한 Object.getOwnPropertyDescriptor() 나 Object.keys() 기능이 있지만, 더 좋은 개발툴 구현을 위해선 reflection API가 필요하다.
  • TypeScript가 일부 reflection 기능을 지원하기 시작하였고, TypeScript 컴파일러는 데코레이터 통해 메타데이터를 내보낼 수 있게 되었다.

2-3-0-2. API 예시

  • 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

2-3-1. 기본형태

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 설정을 수정하여, 프로퍼티의 변경을 방지할 수도 있다.

2-3-2. 응용형태 (1)

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-3. 응용형태 (2)

// 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

2-4. 파라미터 데코레이터


3. 데코레이터의 중첩

3-1. 원리

3-2. 예제

3-2-1. 기본형태

3-2-2. 응용형태


0. 부록

미해결 질문

  • 프로퍼티 데코레이터의 리턴은 property descriptor 형태이다 ?
  • descriptor.value 는 왜 메소드를 가리키는가?
  • meta-data를 사용해야 할 수 밖에 없는 이유는 무엇인가?

공부해야할 주제

  • metadata-reflection
  • Nominal typing VS Structural typing(Duck typing)

참고한 링크

0개의 댓글