NestJS - TypeScript의 데코레이터 활용하기

Tae Yun Choi·2023년 3월 1일
0

개발새발 NestJS

목록 보기
1/2
post-thumbnail

NestJS에 대해 공부하며 필수로 알아야 할 Decorator에 관하여 쓰는 글입니다.

Decorator

데코레이터란 클래스, 메소드, 매개변수, 접근자, 속성에 같이 선언할 수 있으며 @expression 형식으로 사용한다.

현재(작성 시간 기준) stage 3에 올라 실험적으로 사용되고 있지만, 이미 수많은 프로젝트에 사용하고 있으며 안정적이기 때문에 사용하는데에 문제는 없다.

// tsconfig.json
{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    ...
  }
}

아직 실험적인 기능이기에 tsconfig파일에 experimentalDecorators 부분을 true로 변경해주어야 사용 가능하다.


데코레이터 팩토리

커스텀 데코레이터를 만들고 싶다면 함수를 반환하는 형태로 만들면 된다.

function helloDecorator() {
  console.log("coming!");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Hello Decorator");
  }
}

class Hello() {
  @helloDecorator()
  world() {
    console.log("called");
  }
}
const hello = new Hello();

hello.world();

/*
OUTPUT
coming!
Hello Decorator
called
*/

데코레이터 합성 (Decorator Composition)

데코레이터가 여러 행일 경우 데코레이터 합성으로 사용되며 평가는 위에서 아래로 평가되고 호출은 아래에서 위로 호출된다.

function first() {
  console.log('first evaluated');
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    console.log('first called');
  };
}

function second() {
  console.log('second evaluated');
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    console.log('second called');
  };
}

class Composition {
  @first()
  @second()
  method() {
    console.log('method called');
  }
}

const composition: Composition = new Composition();

composition.method();

/*
OUTPUT
first evaluated
second evaluated
second called
first called
method called
*/

1. Class Decorator

function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    reportingURL = "http://www...";
  };
}
 
@reportableClassDecorator
class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
}
 
const bug = new BugReport("Needs dark mode");
console.log(bug) 

/*
	Output
	{ 
		type: 'report', 
		title: 'Needs dark mode', 
		reportingURL: 'http://example.com'
	}
*/

클래스 데코레이터는 이름 그대로 클래스 바로 앞에 선언된다. 클래스 생성자에 적용되어 클래스 정의를 읽거나 수정할 수 있다. 공식 문서에 있는 위의 코드는 클래스 생성자를 데코레이터의 매개변수로써 활용한다. 해당 생성자 함수 내부에서 클래스의 속성을 추가해준 뒤 리턴해주고 있다.
이떄 클래스의 타입이 변경되는건 아니며, 타입시스템은 reportingURL을 인식하지 못하기에 bug.reportingURL과 같이 직접접근은 불가하다.


2. Method Decorator

메소드 데코레이터는 메소드 바로 앞에 선언된다. 메소드의 PropertyDescriptor에 적용되며 메서드의 정의를 읽고 수정하는 것이 가능하다.

function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
 
  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

데코레이터에서 매개변수로 사용하고 있는 target은 메소드가 속해있는 클래스의 생성자와 프로토타입을 가지는 객체이다. propertyKey는 해당 메소드의 이름을 나타내며, descriptor는 메소드가 가지고 있던 설명자객체이다. 따라서 해당 객체의 enumerable의 값은 false로 바뀌게 되고 iterable하지 못하게 된다.




3. Access Decorator

function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }
 
  @configurable(false)
  get x() {
    return this._x;
  }
 
  @configurable(false)
  get y() {
    return this._y;
  }
}	

접근자 데코레이터 또한 메소드 데코레이터와 같이 속성 설명자에 적용되고 접근자의 정의를 읽거나 수정할 수 있다. 위 예시 코드에선 데코레이터를 통해 해당 접근자의 속성 정의를 수정할 수 없도록 변경하였기에 더이상 해당 접근자의 속성을 변경할 수 없다.

4. Property Decorator

function format(formatString: string) {
  return function (target: any, propertyKey: string): any {
    let value = target[propertyKey];
    function getter() {
      return `${formatString} ${value}`;
    }

    function setter(newVal: string) {
      value = newVal;
    }

    return {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    };
  };
}

class Greeter2 {
  @format('world')
  @format('hello')
  greeting: string;
}

const greet = new Greeter2();
greet.greeting = 'world';
console.log(greet.greeting);

/* output
world world
*/

프로퍼티 Decorator는 프로퍼티 선언 바로 위에서 사용된다. 프로퍼티 Decorator는 프로퍼티 선언을 수정하거나 대체할 수 있다.

5. Parameter Decorator

import { BadRequestException } from '@nestjs/common';

function MinLength(min: number) {
  return function (target: any, propertyKey: string, parameterIndex: number) {
    target.validators = {
      minLength: function (args: string[]) {
        return args[parameterIndex].length >= min;
      },
    };
  };
}

function Validate(
  target: any,
  propertyKey: string,
  discriptor: PropertyDescriptor,
) {
  const method = discriptor.value;

  discriptor.value = function (...args) {
    Object.keys(target.validators).forEach((key) => {
      if (!target.validators[key](args)) {
        throw new BadRequestException();
      }
    });
    method.apply(this, args);
  };
}

class User {
  public name: string;

  @Validate
  setName(@MinLength(3) name: string) {
    this.name = name;
  }
}

const user: User = new User();
user.setName('cxxxtxxyxx');
console.log(user.name);

console.log('-----------');
user.setName('ct');
console.log(user.name);
/* output
cxxxtxxyxx
-----------
BadRequestException: Bad Request
*/

매개 변수 Decorator는 매개 변수 선언 바로 위에서 사용된다. 매개 변수 Decorator는 매개 변수 선언을 수정하거나 대체할 수 있다.

profile
hello dev!!

1개의 댓글

comment-user-thumbnail
2023년 3월 7일

헤어응...

답글 달기