본문 바로가기

FrontEnd

타입스크립트 데코레이터 완벽 가이드[A Complete Guide to TypeScript Decorators]

반응형

공식 문서에서 추천하는 커뮤니티 가이드의 번역입니다.

https://saul-mirone.github.io/a-complete-guide-to-typescript-decorator/

 

A Complete Guide to TypeScript Decorators

Decorators make your code leaner.

saul-mirone.github.io

데코레이터는 TypeScript의 세계를 더 멋지게 만듭니다.
사람들은 Angular 및 Nestjs와 같이 이 멋진 기능을 기반으로 구축된 많은 라이브러리를 사용합니다.
이 블로그에서 나는 많은 세부 사항을 가진 데코레이터를 탐색할 것입니다.
이 블로그를 읽은 후 이 강력한 기능을 언제, 어떻게 사용해야 하는지 알 수 있기를 바랍니다. 

TLDR

데코레이터는 고차함수에 불과하다
메타데이터는 클래스, 변수, 메서드 타입에 대한 정보를 런타임에 사용하기 위한 도구다. 리덕스의 type 필드와 별 차이없다.

개요

데코레이터는 다음에 적용될 수 있는 특정 형식의 함수일 뿐입니다.
  1. Class (클래스)
  2. Class Property (클래스 속성)
  3. Class Method (클래스 메서드)
  4. Class Accessor (클래스 접근자)
  5. Class Method Parameter (클래스 메서드 파라미터)
따라서 데코레이터를 적용하는 것은 고차 함수나 클래스와 마찬가지로 일련의 함수를 합성하는 것과 비슷합니다.
데코레이터를 사용하면 proxy pattern을 쉽게 구현하여 코드를 줄이고 멋진 작업을 수행할 수 있습니다.
 
데코레이터의 구문은 매우 간단합니다.
사용하려는 데코레이터 앞에 @ 연산자를 추가하면 데코레이터가 대상에 적용됩니다.
function simpleDecorator() {
  console.log('---hi I am a decorator---')
}

@simpleDecorator
class A {}
사용할 수 있는 데코레이터에는 다섯 가지 유형이 있습니다.
  1. Class Decorators (클래스 데코레이터)
  2. Property Decorators (속성 데코레이터)
  3. Method Decorators (메서드 데코레이터)
  4. Accessor Decorators (접근자 데코레이터)
  5. Parameter Decorators (파라미터 데코레이터)
다음은 다섯 가지 데코레이터 타입을 모두 포함하는 클래스의 예시입니다.
@classDecorator
class Bird {
  @propertyDecorator
  name: string;
  
  @methodDecorator
  fly(
    @parameterDecorator
      meters: number
  ) {}
  
  @accessorDecorator
  get egg() {}
}

평가 & 호출

데코레이터 합성

만약 여러개의 데코레이터를 사용한다면 수학에서의 함수 합성과 같이 적용됩니다. 다음 데코레이터 선언의 합성 결과는 f(g(x))와 같습니다.

@f
@g
test

여러 데코레이터를 사용할 때 다음 단계가 수행됩니다.

  1. 각 데코레이터의 표현은 위에서 아래로 평가(evaluate)됩니다.
  2. 그런 다음 결과는 아래에서 위로 함수로 호출(call)됩니다.

다음 예의 출력 결과를 보면 합성순서에 대해 이해를 높일 수 있을 것입니다.

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

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

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

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
method is called

평가 시점

데코레이터는 클래스 정의가 런타임에 처음 평가될 때 한 번만 실행됩니다. 예를 들어:
function f(C) {
  console.log('apply decorator')
  return C
}

@f
class A {}

// output: apply decorator
이 코드는 클래스 A의 새 인스턴스를 초기화(new A())한 적이 없더라도 '데코레이터 적용'을 로깅합니다.

호출 순서

다양한 타입의 데코레이터에 대한 호출 순서는 다음과 같이 잘 정의되어 있습니다.

  1. 파라미터 데코레이터 메서드에 이어 접근자, 속성 데코레이터가 각 인스턴스 멤버에 적용됩니다.
    • 평가 순서는 메서드/접근자/속성 > 매개변수
  2. 파라미터 데코레이터에 이어 메서드, 접근자, 속성 데코레이터가 각 정적 멤버에 적용됩니다.
    • 평가 순서는 메서드/접근자/속성 > 매개변수
  3. 파라미터 데코레이터가 생성자 > 클래스 데코레이터가 클래스에 적용됩니다.
    • 평가 순서는 클래스 > 생성자

속성/접근자/메서드 데코레이터의 호출 순서는 코드에서 나타나는 순서에 따라 달라집니다.

주 : 즉 안에서 밖으로, 위에서 아래로, 구체적인 것부터 추상적인 것 순서네요
예를 들어 다음 코드를 고려하면,
function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

@f("Class Decorator")
class C {
  @f("Static Property")
  static prop?: number;

  @f("Static Method")
  static method(@f("Static Method Parameter") foo) {}

  constructor(@f("Constructor Parameter") foo) {}

  @f("Instance Method")
  method(@f("Instance Method Parameter") foo) {}

  @f("Instance Property")
  prop?: number;
}

 

평가/호출 순서는다음과 같습니다.
evaluate:  Instance Method
evaluate:  Instance Method Parameter
call:  Instance Method Parameter
call:  Instance Method
evaluate:  Instance Property
call:  Instance Property
evaluate:  Static Property
call:  Static Property
evaluate:  Static Method
evaluate:  Static Method Parameter
call:  Static Method Parameter
call:  Static Method
evaluate:  Class Decorator
evaluate:  Constructor Parameter
call:  Constructor Parameter
call:  Class Decorator

인스턴스 속성에 대한 데코레이터 호출은 인스턴스 메서드보다 늦은 반면
정적 속성에 대한 호출은 정적 메서드보다 빠릅니다.
이는 속성/접근자/메서드 데코레이터의 평가 순서가 코드에서 나타나는 순서에 따라 달라지기 때문입니다.

 

그러나 동일한 메서드 또는 생성자 내의서로 다른 파라미터에 대한 데코레이터 호출 순서는 반대입니다.
여기서 마지막 매개변수 데코레이터가 먼저 호출됩니다.

(평가는 앞에 있는 것부터)

function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  method(
    @f("Parameter Foo") foo,
    @f("Parameter Bar") bar
  ) {}
}
위의 코드는 다음 메시지를 인쇄합니다.
evaluate:  Parameter Foo
evaluate:  Parameter Bar
call:  Parameter Bar
call:  Parameter Foo

여러 데코레이터 합성하기

단일 대상에 여러 데코레이터를 적용할 수 있습니다. 합
합성된 데코레이터의 평가 순서는 다음과 같습니다.

  1. Outer Decorator Evaluate
  2. Inner Decorator Evaluate
  3. Inner Decorator Call
  4. Outer Decorator Call
예를 들어:
function f(key: string) {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  @f("Outer Method")
  @f("Inner Method")
  method() {}
}
위의 코드는 다음 메시지를 인쇄합니다.
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method

정의

클래스 데코레이터

타입 정의

type ClassDecorator = <TFunction extends Function>
  (target: TFunction) => TFunction | void;

@Params:

  • target: 클래스 생성자

@Returns:

  • 클래스 데코레이터가 값을 반환하면 클래스 선언을 대체합니다.

따라서 일부 속성이나 메서드로 기존 클래스를 확장하는 데 적합합니다.
예를 들어 모든 클래스에 대해 toString 메서드를 추가하여 원래의 toString 메서드를 덮어쓸 수 있습니다.

type Consturctor = { new (...args: any[]): any };

function toString<T extends Consturctor>(BaseClass: T) {
  return class extends BaseClass {
    toString() {
      return JSON.stringify(this);
    }
  };
}

@toString
class C {
  public foo = "foo";
  public num = 24;
}

console.log(new C().toString())
// -> {"foo":"foo","num":24}
타입 안전한 데코레이터를 정의할 수 없다는 것은 유감입니다. 즉, 다음을 의미합니다.
declare function Blah<T>(target: T): T & {foo: number}

@Blah
class Foo {
  bar() {
    return this.foo; // Property 'foo' does not exist on type 'Foo'
  }
}

new Foo().foo; // Property 'foo' does not exist on type 'Foo'
이것은 Typescript에서 잘 알려진 문제입니다.
지금 우리가 할 수 있는 것은 대상 클래스에 의해 확장될 타입 정보를 클래스에 제공하는 것입니다.
declare function Blah<T>(target: T): T & {foo: number}

class Base {
  foo: number;
}

@Blah
class Foo extends Base {
  bar() {
    return this.foo;
  }
}

new Foo().foo;

속성 데코레이터

타입 정의

type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void;

@Params:

  • target: 정적 멤버에 대한 클래스의 생성자 함수, 또는 인스턴스 멤버에 대한 클래스의 프로토타입.
  • propertyKey: 속성의 이름입니다.

@Returns:

  • 리턴 값은 무시합니다.
정보 수집에 사용하는 것 외에도
속성 데코레이터를 사용하여 클래스에 일부 메서드나 속성을 추가할 수도 있습니다.
예를 들어 일부 속성의 변경 사항을 수신하는 기능을 추가하는 데코레이터를 작성할 수 있습니다.
function capitalizeFirstLetter(str: string) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function observable(target: any, key: string): any {
  // prop -> onPropChange
  const targetKey = "on" + capitalizeFirstLetter(key) + "Change";

  target[targetKey] =
    function (fn: (prev: any, next: any) => void) {
      let prev = this[key];
      Reflect.defineProperty(this, key, {
        set(next) {
          fn(prev, next);
          prev = next;
        }
      })
    };
}

class C {
  @observable
  foo = -1;

  @observable
  bar = "bar";
}

const c = new C();

c.onFooChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`))
c.onBarChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`))

c.foo = 100; // -> prev: -1, next: 100
c.foo = -3.14; // -> prev: 100, next: -3.14
c.bar = "baz"; // -> prev: bar, next: baz
c.bar = "sing"; // -> prev: baz, next: sing

메서드 데코레이터

타입 정의

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

@Params:

  • target: 정적 멤버에 대한 클래스의 생성자 함수, 또는 인스턴스 멤버에 대한 클래스의 프로토타입.
  • propertyKey: 속성의 이름입니다.
  • descriptor : 멤버의 속성 설명자(descriptor).
interface PropertyDescriptor {
  configurable?: boolean;  // 속성의 정의를 수정할 수 있는지 여부
  enumerable?: boolean;    // 열거형인지 여부
  value?: any;             // 속성 값
  writable?: boolean;      // 수정 가능 여부
  get?(): any;             // getter
  set?(v: any): void;      // setter
}

@Returns:

  • 값을 반환하면 멤버의 설명자로 사용됩니다.
메서드 데코레이터와 속성 데코레이터의 차이는
원래 구현을 재정의하고 몇 가지 공통 로직을 주입할 수 있게 해주는 속성 설명자 매개변수입니다.
예를 들어, 입력 및 출력을 로그아웃하는 일부 메소드에 대해 로거를 추가할 수 있습니다.
function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (...args) {
    console.log('params: ', ...args);
    const result = original.call(this, ...args);
    console.log('result: ', result);
    return result;
  }
}

class C {
  @logger
  add(x: number, y:number ) {
    return x + y;
  }
}

const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3

접근자 데코레이터

type Accessorecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

접근자 데코레이터는 메서드 데코레이터와 유사합니다.
유일한 차이점은 설명자의 키입니다.

메소드 데코레이터의 디스크립터에는 다음과 같은 키가 있습니다.

  • value
  • writable
  • enumerable
  • configurable
접근자 데코레이터의 디스크립터에는 다음 키가 있습니다.
  • get
  • set
  • enumerable
  • configurable
예를 들어 데코레이터를 이용해 해당 속성을 불변으로 만들 수 있습니다.
function immutable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.set;

  descriptor.set = function (value: any) {
    return original.call(this, { ...value })
  }
}

class C {
  private _point = { x: 0, y: 0 }

  @immutable
  set point(value: { x: number, y: number }) {
    this._point = value;
  }

  get point() {
    return this._point;
  }
}

const c = new C();
const point = { x: 1, y: 1 }
c.point = point;

console.log(c.point === point)
// -> false

파라미터 데코레이터

타입 정의

type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;

@Params:

  • target: 정적 멤버에 대한 클래스의 생성자 함수, 또는 인스턴스 멤버에 대한 클래스의 프로토타입.
  • propertyKey: 속성의 이름입니다(메서드 이름, 파라미터 명 아님)
  • parameterIndex: 함수의 매개변수 목록에 있는 매개변수의 서수 인덱스.

@Returns:

  • 리턴 값은 무시합니다.

일반적으로 다른 데코레이터가 사용할 수 있는 정보를 기록하는 데 사용. 혼자선 잘 안씀

요약

클래스 데코레이터 : 기존 클래스를 확장하여 신규 클래스 생성

속성 데코레이터 : 기존 클래스에 속성, 메서드 추가

메서드 데코레이터 : 메서드의 구현을 재정의

접근자 데코레이터 : getter, setter 프록시, 해당 속성 설정 변경

파라미터 데코레이터 :일반적으로 다른 데코레이터가 사용할 수 있는 정보를 기록하는 데 사용. 혼자선 잘 안씀

조합하여 사용하기

일부 복잡한 경우에는 서로 다른 타입의 데코레이터를 함께 사용해야 할 수도 있습니다.
예를 들어 API에 대해 정적 타입 검사기와 런타임 타입 검사기를 모두 추가하려는 경우입니다.
기능을 구현하는 3단계는 다음과 같습니다.
  1. 검증이 필요한 파라미터 표시 (파라미터 데코레이터가 메서드 데코레이터보다 먼저 평가되기 때문에).
  2. 메소드의 디스크립터 값을 변경하고, 메소드 전에 매개변수 유효성 검사기를 실행하고, 실패하면 오류를 던집니다.
  3. 원래 메서드를 실행합니다.
type Validator = (x: any) => boolean;

// save the marks
const validateMap: Record<string, Validator[]> = {};

// 1. mark the parameters need to be validated
function typedDecoratorFactory(validator: Validator): ParameterDecorator {
  return (_, key, index) => {
    const target = validateMap[key as string] ?? [];
    target[index] = validator;
    validateMap[key as string] = target;
  }
}

function validate(_: Object, key: string, descriptor: PropertyDescriptor) {
  const originalFn = descriptor.value;
  descriptor.value = function(...args: any[]) {

    // 2. run the validators
    const validatorList = validateMap[key];
    if (validatorList) {
      args.forEach((arg, index) => {
        const validator = validatorList[index];

        if (!validator) return;

        const result = validator(arg);

        if (!result) {
          throw new Error(
            `Failed for parameter: ${arg} of the index: ${index}`
          );
        }
      });
    }

    // 3. run the original method
    return originalFn.call(this, ...args);
  }
}

const isInt = typedDecoratorFactory((x) => Number.isInteger(x));
const isString = typedDecoratorFactory((x) => typeof x === 'string');

class C {
  @validate
  sayRepeat(@isString word: string, @isInt x: number) {
    return Array(x).fill(word).join('');
  }
}

const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error
이 사례에서 알 수 있듯이 평가 순서뿐만 아니라 다양한 데코레이터의 역할을 이해하는 것이 중요합니다.

메타데이터

엄밀히 말하면 메타데이터와 데코레이터는 ECMAScript의 다른 두 부분입니다.
그러나 reflection과 같은 것을 가지고 놀고 싶다면 항상 두 가지가 모두 필요합니다.

 

이전 예를 살펴보세요.

다른 타입의 유효성 검사기(validator)를 작성하고 싶지 않다면 어떻게 될까요?
메서드의 타입을 이용해 유효성 검사기를 유추할 수 있는 유효성 검사기를 하나만 작성할 수 있을까요?

reflect-metadata의 도움으로 디자인 타임 타입을 얻을 수 있습니다.
import 'reflect-metadata';

function validate(
  target: Object,
  key: string,
  descriptor: PropertyDescriptor
) {
  const originalFn = descriptor.value;

  // get the design type of the parameters
  const designParamTypes = Reflect
    .getMetadata('design:paramtypes', target, key);

  descriptor.value = function (...args: any[]) {
    args.forEach((arg, index) => {

      const paramType = designParamTypes[index];

      const result = arg.constructor === paramType
        || arg instanceof paramType;

      if (!result) {
        throw new Error(
          `Failed for validating parameter: ${arg} of the index: ${index}`
        );
      }
    });

    return originalFn.call(this, ...args);
  }
}

class C {
  @validate
  sayRepeat(word: string, x: number) {
    return Array(x).fill(word).join('');
  }
}

const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error
현재로서는 세 가지 타입의 디자인 타임 타입 주석을 얻을 수 있습니다.
  • design:type: 속성의 타입
  • design:paramtypes: 메서드 파라미터들의 타입
  • design:returntype: 메서드의 리턴타입

이 세 가지 타입의 결과는 생성자 함수(예: String 및 Number)입니다.

규칙은 다음과 같습니다.

  • number -> Number
  • string -> String
  • boolean -> Boolean
  • void/null/never -> undefined
  • Array/Tuple -> Array
  • Class -> The constructor function of the class
  • Enum -> Number when pure number enum, or Object
  • Function -> Function
  • 그 외는  Object 입니다.

언제 사용하나요?

이제 데코레이터를 언제 사용해야 하는지 결론을 내릴 수 있습니다.
몇 가지 사용 사례를 나열하고 싶습니다.
이 블로그를 읽은 후 더 많은 사용 사례를 파악하고 데코레이터를 사용하여 코드를 단순화할 수 있기를 바랍니다.

  • Hooks 전/후.
  • 속성 변경 및 메서드 호출을 관찰
  • 매개변수 변환
  • 추가 메서드 또는 속성을 추가
  • 런타임 타입 검증
  • 자동 직렬화 및 역직렬화
  • 의존성 주입

참고

https://wikidocs.net/158481

 

2.6 데코레이터

Nest는 데코레이터를 적극 활용합니다. 데코레이터를 잘 사용하면 횡단관심사를 분리하여 관점 지향 프로그래밍을 적용한 코드를 작성할 수 있습니다. 타입스크립트의 데코레이터는 ...

wikidocs.net

 

반응형