sogno
카테고리
작성일
2023. 2. 27. 18:07
작성자
sognociel

유데미 【한글자막】 Typescript :기초부터 실전형 프로젝트까지 with React + NodeJS 강의와 inpa님 블로그를 참고하여 작성..!

inpa님 블로그 | 링크

 

1. 데코레이터

데코레이터는 코드 조각을 장식해주는 역할을 하며 타입스크립트에서 그 기능을 함수로 구현할 수 있다.

예를들어 메소드 / 클래스 / 프로퍼티 / 파라미터 위에 @함수 를 장식해줌으로써, 코드가 실행(런타임)이 되면 데코레이터 함수가 실행되어, 장식한 멤버를 보다 파워풀하게 꾸며주는 것으로 이해하면 된다.

 

TypeScript 공식 문서 설명

「TypeScript 및 ES6에 클래스가 도입됨에 따라, 클래스 및 클래스 멤버에 어노테이션을 달거나 수정하기 위해 추가 기능이 필요한 특정 시나리오가 있습니다. 데코레이터는 클래스 선언과 멤버에 어노테이션과 메타-프로그래밍 구문을 추가할 수 있는 방법을 제공합니다. 데코레이터는 JavaScript에 대한 2단계 제안이며 TypeScript의 실험적 기능으로 이용 가능합니다.」

 

  • 데코레이터는 클래스 선언, 메서드(method), 접근자(accessor), 프로퍼티(property) 또는 매개변수(parameter)에 첨부할 수 있는 특수한 종류의 선언
  • 데코레이터 함수에는 target(현재타겟), key(속성이름), descriptor(설명)가 전달 (단, 어떤 멤버를 장식했느냐에 따라 인수가 달라짐)
  • 메소드나 클래스 인스턴스가 만들어지는 런타임에 실행된다. 즉, 매번 실행되지 않는다.
  • 데코레이터는 클래스 또는 클래스 내부의 생성자, 프로퍼티 , 접근자, 메서드, 그리고 매개변수에만 장식될 수 있다.
function readonly(writable: boolean) {
   return function (target: any, decoratedPropertyName: any): any {
      return {
         writable: !writable,
      };
   };
}

class Test {
   property = 'property';

   @readonly(false)
   public data1 = 0;

   @readonly(true)
   public data2 = 0;
}

const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.

데코레이터 패턴은 클래스를 수정하지 않고 클래스의 멤버들의 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴의 하나이다. 데코레이터 패턴을 사용하면 전체 기능에 신경 쓰지 않고 특정 인스턴스에 초점을 맞출 수 있다.

 

 

데코레이터를 활성화하려면 명령줄 또는 tsconfig.json에서 실험적 데코레이터 컴파일러 옵션을 활성화해야 한다.

$ tsc --target ES6 --experimentalDecorators

 

tsconfig.json 파일 수정

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true
  }
}

 

데코레이터는 @expression 형식을 사용하는데, 여기서 expression은 데코레이팅 된 선언에 대한 정보와 함께 런타임에 호출되는 함수여야 한다. (함수 이름... 적으면 됨. 보통 첫 글자는 대문자로 작성한다고 하는데 소문자도 상관없다고 한다)

function Logger(constructor: Function) {
  console.log('Logging...');
  console.log(constructor);
}

@Logger
class Person {
  name = 'Max';

  constructor() {
    console.log('Creating person object...');
  }
}

const pers = new Person();

console.log(pers);

 

 

 

2. 데코레이터 팩토리

데코레이터 팩토리 함수는 데코레이터 함수를 감싸는 래퍼 함수로 보통 데코레이터가 선언에 적용되는 방식을 원하는 대로 바꾸고 싶을때 사용된다. 데코레이터 팩토리는 단순히 데코레이터가 런타임에 호출할 표현식을 반환하는 함수이다.

프로그래밍에서 함수에게 사용자가 인자를 전달할 수 있는 것과 유사하게, 데코레이터 함수 또한 팩토리를 사용해 사용자로부터 인자를 전달 받도록 설정할 수 있다.

 

일반적으로 데코레이터에서는 팩토리를 만들어 사용하고 있기 때문에 거의 디폴트처럼 사용된다고 보면 된다.

// 데코레이터 팩토리
function Logger(logString: string) {
  // 데코레이터 함수
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

@Logger("LOGGING - PERSON")
class Person {
  name = "Max";

  constructor() {
    console.log("Creating person object...");
  }
}

const pers = new Person();

console.log(pers);

 

 

 

3. 데코레이터 합성

선언에 여러 개의 데코레이터를 사용할 수 있다.

 

그렇다면 데코레이터들의 실행 순서는?

function Logger(logString: string) {
  console.log("Logger return 앞");
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

function WithTemplate(template: string, hookId: string) {
  console.log("WithTemplate return 앞");
  return function (_: Function) { //_를 입력해서 존재는 알지만 쓰지 않겠다고 명시하는것
    console.log(hookId);
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  };
}

@Logger("logString")
@WithTemplate("<h1>My Person Object</h1>", "app")

Logger, WithTemplate 두 개의 데코레이터가 있고, class 앞에서 두 데코레이터를 호출하면 아래와 같은 결과가 나온다.

빨간 선을 기준으로 먼저 데코레이터들의 return부 앞에 있는 코드가 실행되고, 그 이후에는 데코레이터의 return부가 역순으로 실행되는 모습을 볼 수 있다.

 

// Size 데코레이터 팩토리
function Size() {
   console.log('Size(): 평가됨');
   // Size 데코레이터
   return function (target: any, prop: string, desc: PropertyDescriptor) {
      console.log('Size(): 실행됨');
   };
}
// Color 데코레이터 팩토리
function Color() {
   console.log('Color(): 평가됨');
   // Color 데코레이터
   return function (target: any, prop: string, desc: PropertyDescriptor) {
      console.log('Color(): 실행됨');
   };
}
// Button 클래스 정의
class Button {
   // 메서드에 멀티 데코레이터 적용
   @Size()
   @Color()
   isPressed() {}
}

Size(): 평가됨
Color(): 평가됨
Color(): 실행됨
Size(): 실행됨

순으로 출력된다.

 

4. 클래스 데코레이터 (Class Decorators)

클래스 데코레이터는 클래스 선언 직전에 선언된다. 클래스 데코레이터는 클래스 생성자에 적용되며 클래스 정의를 관찰, 수정 또는 교체하는 데 사용할 수 있다. 클래스를 수정하거나 교체한다는 것은, 클래스 데코레이터는 클래스를 반환할 수가 있는데 옛 클래스를 대체, 확장할 수 있다는 것이다.

 

클래스 반환 클래스 데코레이터의 예시

function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor { // 기존의 클래스에서 reportingURL이라는 요소 추가
    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.title); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
 
bug.reportingURL; // Error! Property 'reportingURL' does not exist on type 'BugReport'.
(bug as any).reportingURL // 이렇게 접근할 수 있다.

데코레이터가 확장된 클래스를 반환하지만, 클래스 자체를 바꾸는 것은 아니기 때문에 bug에는 reportingURL 속성이 없다. (bug as any).reportingURL 로 접근이 가능하다.

 

 

 

5. 메서드 데코레이터 (Method Decorators)

메서드 데코레이터는 메서드 선언 직전에 선언 된다. 메서드 관찰, 수정 또는 대체하는 데 사용할 수 있으며 가장 많이 이용되는 데코레이터라고 한다. 클래스 데코레이터는 클래스(생성자 함수)를 extends 하는 방법으로 기능을 확장할 수 있었지만, 메서드 데코레이터는 메서드의 Property Descriptor를 수정하여 메서드를 확장한다.

 

메서드 데코레이터는 총 3개의 인자를 전달받는다.

  • 첫 번째 argument : static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  • 두 번째 argument : 해당 method의 이름
  • 세 번째 argument : 해당 method의 property descriptor 
function methodDecorator(
  target: any, // static 메서드라면 클래스의 생성자 함수, 인스턴스의 메서드라면 클래스의 prototype 객체
  propertyKey: string, // 메서드 이름
  descriptor: PropertyDescriptor // 메서드의 Property Descriptor
) {
	... 
} // return 값 무시됨

 

만약 해당 메서드가 호출될 때 특정 무엇인가를 동작시키고 싶다면 데코레이터에서 descriptor를 다음과 같이 재정의 해주면 된다.

Property Description
value 현재 값 value
writable 수정 가능하면 true, 아니면 false
enumarable for (i in [1, 2, 3])과 같이 순회가 가능하면 true, 아니면 false
configurable Property definition이 수정 및 삭제가 가능하면 true, 아니면 false

 

function methodDecorator() {
   return function (target: any, property: string, descriptor: PropertyDescriptor) {

      // descriptor.value는 test() 함수 자체를 가리킨다. 이 함수를 잠시 변수에 피신 시킨다.
      let originMethod = descriptor.value; 

      // 그리고 기존의 test() 함수의 내용을 다음과 같이 바꾼다.
      descriptor.value = function (...args: any) {
         console.log('before');
         originMethod.apply(this, args); // 위에서 변수에 피신한 함수를 call,apply,bind 를 통해 호출
         console.log('after');
      };
   };
}

class Test {
   property = 'property';
   hello: string;

   constructor(m: string) {
      this.hello = m;
   }

   @methodDecorator()
   test() {
      console.log('test');
   }
}

let test = new Test("world")
test.test()
[Running] ts-node "index.ts"
before
test
after

 

데코레이터 인수의 descriptor.value 로 접근하면 장식된 함수 자체(Test 클래스의 test 메소드)로 접근하게 되는데 이를 잠시 백업하고 나중에 실행하는 식으로 응용이 가능하다. 백업된 메서드를 호출할 때 apply를 이용하여 this를 바인딩해줘야 한다. 여기서 this는 메서드를 호출하는 객체를 의미하는데, 만일 this를 바인딩하지 않으면 해당 메서드를 어느 객체가 호출 했는지 알 수 없다.


 

6. 프로퍼티 데코레이터 (Property Decorators)

프로퍼티 데코레이터는 프로퍼티 선언 바로 전에 선언 된다. 메서드 데코레이터와 다르게 데코레이터 함수에 Property Descriptor 가 인자로서 제공되지 않는다는 차이가 있다. 

function propertyDecorator(
  target: any, // static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  propName: string, // 프로퍼티 이름
) {
	...
} // return하는 값이 Property Descriptor 형태 혹은 void.
function Log(target: any, propertyName: string | Symbol) {
  console.log("Property Decorator");
  console.log(target, propertyName);
}

class Product {
  @Log
  title: string;
  private _price: number;

  set price(val: number) {
    if (val > 0) {
      this._price = val;
    } else {
      throw new Error("Invalid pricd - should be positive");
    }
  }

  constructor(t: string, p: number) {
    this.title = t;
    this._price = p;
  }

  getPriceWithTax(tax: number) {
    return this._price * (1 + tax);
  }
}

target은 title

 

 

 

7. 접근자 데코레이터 (Accessor Decorators)

접근자 데코레이터는 접근자 선언 바로 전에 선언되며 getter, setter 함수의 앞에 사용하는듯하다.

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;
  }
}

 

 

 

8. 매개변수 데코레이터 (Parameter Decorators)

매개변수 데코레이터는 함수의 매개변수 왼쪽 옆에 명시해 주면 된다.(가로로 써도 인식되기 때문에...)

프로퍼티나 접근자 데코레이터와는 다르게 세 번째 인자를 descriptor가 아니라 parameterIndex로 받는다.

function parameterDecorator(
  target: any, // static 메서드의 파라미터 데코레이터라면 클래스의 생성자 함수, 인스턴스의 메서드라면 prototype 객체
  methodName: string, // 매개변수가 들어있는 메서드의 이름
  paramIndex: number // 매개변수의 순서 인덱스
) {
    ...
} // 리턴값은 무시됨

 

function parameterDecorator(target: any, methodName: string, paramIndex: number) {
   console.log('parameterDecorator start');
   console.log(target);
   console.log(methodName);
   console.log(paramIndex);
   console.log('parameterDecorator end');
}

class Test7 {
   private _name: string;
   private _age: number;

   constructor(name: string, @parameterDecorator age: number) {
      this._name = name;
      this._age = age;
   }

   print(@parameterDecorator message: string) {
      console.log(message);
   }
}