배운 것

타입스크립트 데코레이터

세발낙지 2022. 6. 1. 18:41

    Nest.js를 활용해서 API서버를 구현할 때, 데코레이터를 많이 사용하였지만 만들어진 사용법 외에는 잘 모르는 것 같아서 데코레이터에 대해서 공부해보았다. 데코레이터는 아직 자바스크립트에서 지원하는 것은 아니고, 타입스크립트에서만 사용할 수 있다. 데코레이터를 활용한 코드를 자바스크립트로 빌드한 것을 보면, Reflect 객체를 활용하여 구현한 것을 볼 수 있다. 데코레이터에 대해서 찾아보면서 Reflect를 참 많이 본 것 같은데, 공부를 해봐야 할 것 같다.


    데코레이터는 기본적으로 함수의 형태로 정의된다.

function Log(target: any) {
  console.log('decorator');
}

@Log
class Person {
  name = 'MAX';
}

    위와 같은 형태로 데코레이터를 만들고 사용할 수 있는데, 데코레이터는 class가 정의될 때 실행된다. 또한 데코레이터는 클래스에 직접 사용하는 것 이외에도 프로퍼티나 메서드, 파라미터에 사용할 수 있다. class에 직접적으로 사용할 경우에는 인자로 constructor를 받는다. 그러나, 프로퍼티나 메서드, 파라미터에 사용하게 될 경우 인자로 받는 것이 각기 다르므로 이에 대해서 알아둘 필요가 있다.

 

    데코레이터는 데코레이터 팩토리를 통해 추가적인 인자를 받아 이용할 수 있다.

function Logger(loggingString: string) {
  return function (constructor: Function) {
    console.log(constructor);
    console.log(loggingString);
  };
}

@Logger('string to log')
class Person {
  name = 'MAXIM';
}

    위와 같은 형태로 데코레이터를 만들게 되면, 추가적인 인자를 받아서 이를 활용한 데코레이터를 익명 함수로 만들어서 사용할 수 있다.이 경우 

 

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

    실제로 Nest.js를 사용하게 되면 @Controller('cats')나 @Get()에서 함수를 실행하는 듯한 표현을 볼 수 있는데 이는 Decorator factory를 만들어서 일정 부분 커스텀된 데코레이터를 리턴하도록 하는 방식으로 데코레이터를 이용한 것으로 볼 수 있다. 마치 객체지향 프로그래밍에서 원하는 class를 정의하고, 필요할 때 상속, 확장하여 사용하는 것과 비슷한 느낌이었다.

function WithTemplate(hookId: string, template: string) {
  return function (constructor: any) {
    const p = new constructor();
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
      console.log(p);
    }
  };
}

@WithTemplate('app', '<h1>데코레이터 실습</h1>')
class Person {
  name = 'MAX';
}

    이런 식으로 추가적인 처리를 할 수 있으며, constructor를 인자로 받기 때문에 새로운 객체를 생성하는 것 또한 가능하다. 또한, 동시에 여러개의 데코레이터를 달아줄 수도 있다. 이 경우 factory는 일반적인 코드처럼 위에서부터 아래로 순차적으로 실행되지만 데코레이터의 경우 아래에서부터 위로 순차적으로 실행된다.

 

    매서드, 프로퍼티, 접근자에 대해서도 데코레이터를 활용할 수 있다고 하였는데 이 경우 각각 받는 인자는 다음과 같다.

function ForProperty(target: any, propertyName: string) {
  // target은 생성자 함수의 prototype이다.
  // static 프로퍼티라면, constructor 함수를 받는다.
  // 두번째 인자는 프로퍼티의 이름이다. (key값)
  console.log(target);
}

function ForAccessor(
  target: any,
  name: string,
  descriptor: PropertyDescriptor
) {
  // target은 생성자 함수의 prototype이다.
  // name은 해당 setter / getter의 대상의 이름이다.
  // descriptor는 해당 프로퍼티에 대한 설명이다.
  console.log(target);
  console.log(name);
  console.log(descriptor);
}

function ForMethod(
  target: any,
  name: string | Symbol,
  descriptor: PropertyDescriptor
) {
  // target은 생성자 함수의 prototype이다.
  // name은 해당 메소드의 이름이다.
  // descriptor는 해당 프로퍼티에 대한 설명이다.
  console.log(target);
  console.log(name);
  console.log(descriptor);
}

function ForParameter(target: any, name: string | Symbol, position: number) {
  // target은 생성자 함수의 prototype이다.
  // name은 파라미터를 받는 함수의 대상의 이름이다.
  // descriptor는 해당 프로퍼티에 대한 설명이다.
  console.log(target);
  console.log(name);
  console.log(position);
}

class Product {
  @ForProperty
  title: string;
  _price: number;
  constructor(title: string, _price: number) {
    this.title = title;
    this._price = _price;
  }
  @ForAccessor
  set price(value: number) {
    if (value >= 0) {
      this._price = value;
    }
  }
  @ForMethod
  getPriceWithTax(@ForParameter tax: number) {
    return this._price * (1 + tax);
  }
}

    데코레이터는 값을 반환할 수 있다.

 

    이 경우에도 위와 마찬가지로 어디에 데코레이터를 사용하느냐에 따라 다르다. class에 직접적으로 사용하는 데코레이터의 경우, 반환값이 constructor 함수를 대체하게 된다. 즉, 다음과 같이 사용할 수 있는 것이다.

function WithTemplate(template: string, hookId: string) {
  console.log('TEMPLATE FACTORY');
  return function<T extends { new (...args: any[]): {name: string} }>(
    originalConstructor: T
  ) {
    return class extends originalConstructor {
      constructor(..._: any[]) {
        super();
        console.log('Rendering template');
        const hookEl = document.getElementById(hookId);
        if (hookEl) {
          hookEl.innerHTML = template;
          hookEl.querySelector('h1')!.textContent = this.name;
        }
      }
    };
  };
}

@WithTemplate('<h1>My Person Object</h1>', 'app')
class Person {
  name = 'Max';

  constructor() {
    console.log('Constructed!');
  }
}

    이 경우에는, constructor를 조작할 수 있기 때문에 객체가 정의될 때가 아니라 객체가 생성될 때 실행될 로직을 작성할 수 있다. 본질적으로는 class를 정의할 때에 실행되지만 이것을 잘 이용하면 생성될 때에도 작동하도록 할 수 있는 것이다.

 

    접근자와 메서드에 이용되는 데코레이터 또한 반환값을 이용할 수 있다. 이 경우, propertyDescriptor를 반환하게 된다. 

 

'배운 것' 카테고리의 다른 글

미확인 도착지  (0) 2022.06.08
자바스크립트의 this란?  (0) 2022.06.06
징검다리 건너기  (0) 2022.05.30
프로그래머스 지형 이동  (0) 2022.05.27
프로그래머스 자물쇠와 열쇠  (0) 2022.02.03