2023. 3. 22. 15:35ㆍ코딩
TypeScript Decorator는 TypeScript의 핵심 기능 중 하나로, 클래스, 메서드, 프로퍼티 등에 메타데이터를 추가하고 동적으로 동작을 변경하는 방법을 제공합니다. Decorator는 앵귤러 프레임워크의 핵심 개념 중 하나이기도 하며, TypeScript를 사용하는 개발자들에게 꼭 알아둬야 할 중요한 개념입니다.
이번 블로그 글에서는 TypeScript Decorator가 무엇인지, 어떻게 사용하는지, 그리고 어떤 실제적인 예시들이 있는지 등에 대해 알아보도록 하겠습니다. 또한, Decorator를 사용하는 이점과 함께 어떻게 좀 더 유연하게 코드를 작성할 수 있는지 등을 다룰 예정입니다. 따라서 TypeScript를 사용하는 개발자라면 Decorator를 알아두면 매우 유용할 것입니다.
- 데코레이터
 - 데코레이터 팩토리
 - 데코레이터 합성
 - 데코레이터 평가
 - 클래스 데코레이터
 - 메서드 데코레이터
 - 접근자 데코레이터
 - 프로퍼티 데코레이터
 - 매개변수 데코레이터
 - 메타데이터
 
데코레이터
데코레이터는 클래스 선언, Method, Accessor, property, parameter에
첨부할 수 있는 특수한 종류의 선언입니다.
- @Expression 형식 [(at icon) + Function name]을 사용합니다.
 
데코레이터 팩토리
데코레이터가 선언에 적용되는 방식을 원하는대로 바꾸고 싶을 때..
그럴 때 작성하는 것으로 런타임에 호출할 표현식을 반환하는 함수 입니다.
function Apple(value: string) { //데코레이터 팩토리
  return function (target) { //데코레이터
    // 위에서 가져온 value와 target으로 어떠한 작업을 수행
  }
}
데코레이터 합성
- 단일행 :   
@A @B function - 다중행 :  
@A @B function 
위에서 선언한 @A @B function은 A(B(function))과 같습니다.
- 위에서 아래로 평가하며, 
A call B call - 아래에서 위로 함수를 호출합니다.
B Complate A Complate
 
클래스 데코레이터 (Class Decorator)
클래스 선언 직전에 선언 합니다.
클래스 정의를 감시, 수정, 교체 하는데 사용 가능합니다.  
- 선언 파일이나 주변 컨텍스트에서 사용할 수 없습니다.
- 컨텍스트 (Context) : 예) 선언 클래스
 
 
클래스 데코레이터는 적용하는 클래스의 생성자를 유일한 인수로 받습니다.
- constructor
 
클래스 데코레이터가 값을 반환하면 생성자 함수로 컨버팅 됩니다.
예제 Class
@classDecorator
class sealed {
  data: string;
  constructor(message : string) {
    this.data = message;
  }
  method() {
    return "Hello, " + this.data;
  }
}
예제 Decorator
function classDecorator(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}
- 예제에서는 Object.seal() 기능을 이용해서 객체를 밀봉하였습니다.
- Object.seal() : 객체 밀봉
- 새로운 속성 추가 x
 - 모든 속성을 설정 불가능 상태로 변경
 - [Object.freeze와 다른 점] 쓰기 가능한 속성의 값은 밀봉 후에도 변경 가능
 
 
 - Object.seal() : 객체 밀봉
 
메서드 데코레이터 (Method decorator)
메서드 선언 직전에 선언합니다.
메서드 정의를 감시, 수정, 교체 하는데 사용 가능합니다.  
- 선언 파일이나 주변 컨텍스트에서 사용할 수 없습니다.
 
클래스 데코레이터와 달리 런타임에 세 개의 인수와 함께 함수로 호출됩니다.
- 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입
 - 멤버의 이름
 - 멤버의 프로퍼티 설명자 (property descriptor)
 
메서드 데코레이터가 값을 반환하면, 메서드의 프로퍼티 설명자로 사용됩니다.
예제 Class
class example {
    a: string = "Hello";
    get b(): string {
        return `${this.a} World`;
    }
    @decorator
    method(c: string): void {
        console.log(c);
    }
}
예제 Decorator
function decorator() {
    return function (
        target: any,
        propertyKey: string, 
        descriptor: propertyDescriptor
    ) {
        console.log(target); // {b: [Getter], method: [Function (anonymous)]}
        console.log(propertyKey); // d
        console.log(descriptor); 
        /**
         * {
         *  value: [Function (anonymous)]
         *  writable: true,
         *  enumerable: true,
         *  configurable: true
         * }
         */ 
    }
}
@decorator 가 실행되면서 예제 코드의 console 결과가 나오는 것입니다.
@decorator를 좀 바꿔본다면
function decorator ( target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
    const method = descriptor.value;
    descriptor.value = function () {
        try {
            method();   
        } catch (error) {
            console.log("error 핸들링 로직");
        }
    }
}
이렇게 바꾸고 반영해서 실행한다면!!
예제 class 중
...
@decorator
method(c: string): void {
    console.log(c);
    throw new Error();
}
...
//데코레이터
...
new example().method("오..");
console
오..
error 핸들링 로직이라는 결과가 나온다.
접근자 데코레이터 (Accessor Decorator)
접근자 선언 직전에 선언
접근자 정의를 감시, 수정, 교체 하는데 사용 가능합니다.
- 문서 순서대로 지정한 첫 번째 접근자에 적용해야 합니다.
- 각각의 선언이 아닌 결합한 property descriptor에 적용되기 때문입니다.
 
 
메서드 데코레이터와 같이 세개의 인수와 함께 함수로 호출합니다.
접근자 데코레이터가 값을 반환하면 프로퍼티 설명자로 사용됩니다.
예제 class (typescript handbook 中)
class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number, public size: string = "100") {
        this._x = x;
        this._y = y;
    }
    @configurable(false)
    get x() { return this._x }
    @configurable(false)
    get y() { return this._x }
}
예제 decorator
function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    }
}
// point 객체 인스턴스 생성
const point = new Point(100, 150);
// 속성 제거 가능
delete point.size;
// [오류] delete 연산자의 피연산자는 읽기 전용 속성일 수 없습니다.
delete point.x;
이 코드에서 생성된 객체 인스턴스의 접근 제어자 속성을 제거하려 시도하면 오류가 발생합니다.
메서드 데코레이터와 동일하나 decription의 configurable 속성을 바꿉니다.
프로퍼티 데코레이터 (Property decorator)
프로퍼티 선언 직전에 선언합니다.
앞선 메서드 & 접근자 데코레이터와 달리 두 개의 인수와 함께 함수로 호출합니다.
- 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입
 - 멤버의 이름
 
예제 class
class Car {
    private name: string;
    private price: number;
    private type: string;
    constructor(name: string, price: number) {
        this.name = name;
        this.price = price;
    }
    public toString() {
        return `${this.name}, ${this.type}, ${this.price}`
    }
}
객체를 주입할 때 사용할 간단한 컨테이너를 정의하고 객체를 넣어둡니다.
class Container {
    private static map: {[key: string]: any} = {};
    static add(key: string, value: string) {
        Container.map[key] = value;
    }
    static get(key: string): string {
        return Container.map[key];
    }
}
Container.add('myType', 'Classic');
console.log(Container.get('myType')); // 'Classic'
이제 예제에서 사용할 데코레이터를 정의합니다.
예제 decorator
function Inject(param: string) {
    return function (target: any, propertyKey: string) {
        console.log(target); // {toString: f, constructor: f}
        console.log(propertyKey); // type
        target[propertyKey] = Container.get(param); //target 오브젝트에 property 값 할당
    }
}
이렇게 정의한 데코레이터는 클래스를 정의할 때 사용할 수 있습니다.
class Car {
    private name: string;
    private price: number;
    @Inject('myType') //값 주입, 위에서 이미 myType에 Classic이라는 값을 넣어놨음
    private type: string;
}
let myCar = new Car('AMG GT', 15000); // type말고 나머지 name과 price에 값 할당
console.log(myCar.toString()); // AMG GT, Classic, 15000
여기서 클래스가 정의될 때 데코레이터 함수가 실행되어 클래스의 프로퍼티 값이 지정되기 때문에 클래스를 위와 같이 정의하면 컨테이너의 값을 수정해도 처음 값이 그대로 출력됩니다.
Container.add('myType', 'Custom');
let myCar = new Car('918 Spider', 150000);
console.log(myCar.toString()); // 918Spider, Classic, 150000
그래서 Car 클래스의 Type property가 값이 아닌 함수를 가지고 있게 수정 하고, 값이 아닌 함수를 return 하게 Inject function을 수정하게 된다면 수정된 컨테이너의 값이 반영 됩니다.
function Inject(param: string) {
    ...
    tartget[propertyKey] = () => Container.get(param); //값이 아닌 함수를 리턴
}
class Car {
    ...
    @Inject('myType')
    private type: Function; //'type'은 값이 아니라 함수
    ...
}
Container.add('myType', 'Custom');
let myCar = new Car('918 Spider', 150000);
console.log(myCar.toString()); // 918Spider, Custom, 150000
매개변수 데코레이터 (Parameter decorator)
매개 변수 선언 직전에 선언 합니다.
세 개의 인수와 함께 함수로 호출 합니다.
- 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입
 - 멤버의 이름
 - 함수 매개 변수 목록에 있는 서수 색인 (Ordinal index)
 
매개 변수 데코레이터의 반환값은 무시 됩니다.
정리하면 파라미터 데코레이터는 옵저빙(감시), 값 변경이 안되기에 metadata를 정의할 때 사용합니다. 그렇기에 reflect metadata랑 같이 사용합니다.
데코레이터 호출 순
function cd() {
    console.log('class');
    ...
}
function md() {
    console.log('method');
    ...
}
function prod() {
    console.log('property');
    ...
}
function paramd() {
    console.log(parameter);
    ...
}
@cd()
class example {
    @prod()
    property = "property";
    @md()
    test(
        @paramd() param: string
    ) {
        console.log('test');
    }
}
결과는
property
method
parameter
class순으로 데코레이터 호출 순서는
Property decorator =>
Method decorator =>
Parameter decorator =>
Class decorator 입니다.
Annotation vs Decorator
자바 어노테이션과 다른 점
문법적으로 봤을 때 java의 annotation과 큰 차이는 없습니다. 다만 차이점이 있다면
decorator는 runtime에서만 역할을 한다는 점 입니다. 
java의 annotation은 Retention이라는 게 있어 compiler에게만 보이고, runtime에는 없어지게 하거나 runtime에도 살아남아서 jvm에 의해 참조할 수 있게 할수도 있습니다.
- @Retention : 어느 시점까지 어노테이션의 메모리를 가져갈 지 설정합니다.
- 어노테이션의 라이프 사이클
 
 - Jvm (Java Virtual Machine) : Java 컴파일러가 bytecode로 변환한 것을 os가 이해해줄 수 있도록 해석해주는 것
 
typescript에서는 정적인 타입이 없기 때문에 애초에 compile time에 기능을 할 수 없습니다. 이런 측면에서 runtime에서만 활용할 수 있는 annotation이라고 볼 수 있습니다.
- Compile Time : 개발자에 의해 작성된 소스코드를 컴퓨터가 인식할 수 있는 기계어 코드로 변환되어 실행 가능한 프로그램이 되는 과정을 의미
 - Run Time : 컴파일 과정을 마친 응용 프로그램이 사용자에 의해서 실행되는 때를 의미
 
메타데이터 Metadata
- 데이터의 데이터
 
Reflection
동일한 시스템(또는 그 자체)의 다른 코드를 검사할 수 있는 코드를 설명하는데 사용됩니다.
Typescript에서의 reflection API
Typescript에서는 reflect-metadata 패키지를 사용하여 메타데이터 리플렉션 API를 사용할 수 있습니다.
npm install reflect-metadata
Typescript의 tsconfig.json 파일의 emitDecoratorMetadat를 true로 설정해야 합니다.
이제 프로퍼티 데코레이터로 한번 적용 해본다면..
예제 decorator
function logtype(target: any, key: string) {
    let t = Reflect.getMetadata("design:type", target, key);
    console.log(`${key} type : ${t.name}`);
}
그리고 클래스에 적용한다면
class izone {
    @logType
    public wizone: string;
}
이제 이렇게 작성한 예제 코드를 실행 한다면
// console
wizone type: String
파라미터 데코레이터에 적용 해봅시당
function logParamTypes(target: any, key: string) {
    let types = Reflect.getMetadata("design:paramtypes", target, key);
    let s = types.map(a => a.name).join();
    console.log(`${key} param types: ${s}`)
}
이제 클래스의 메서드 중 하나에 이 함수를 적용해보면
class Ive {}
interface Dive {}
class Lovedive{
  @logParamTypes
  narcissistic(
    yujin: string,
    gaeul: number,
    rei: Dive,
    wonyoung: Ive,
    liz: Function,
    leeseo: { baby: string },
  ): number {
    return 1
  }
}
이제 위 예제를 실행하게 되면
//console
narcissistic param types : string, number, Object, Ive, Function, Object
design: returntype 이라는 메타 데이터 키를 사용하여 메서드의 반환 유형에 대한 정보를 얻을 수도 있습니다.
메타데이터 활용 예제
메소드, 파라미터 데코레이터에서 메타데이터를 이용한 valdation 예제
메서드 데코레이터
- 받은 정보들을 토대로 메타데이터에서 가져옵니다.
 
- 가져온 데이터가 있다면, 해당 아이템의 유형에 따라 정규식 변경
 
- 정규식 test를 통해 성공 여부를 아이템의 status에 저장
 
function validateDecorator(
  target: any,
  propertyName: string,
  descriptor: TypedPropertyDescriptor<any>
) {
  let method = descriptor.value;
  descriptor.value = function () {
    let capitalParameters: number[] = Reflect.getOwnMetadata(
      checkCapitalMetadataKey,
      target,
      propertyName
    ); //metadata에서
    // CheckValidate Symbol metadata 키를 가지고,
    // 멤버에 대한 생성자 함수, 프로토타입 타겟에
    // checkValidate라는 property 키를 가진 애를 가져오기~
    if (capitalParameters) {
      // metadata에 저장이 되어있으면 ?
      for (let parameterIndex of capitalParameters) {
        //루프를 돌려~
        let regex_text = ""; // 정규식 문자 text
        switch (arguments[parameterIndex].type) {
          case validType.English:
            regex_text = regex.English;
            break;
          case validType.Korean:
            regex_text = regex.Korean;
            break;
          case validType.Number:
            regex_text = regex.Number;
            break;
          case validType.Email:
            regex_text = regex.Email;
            break;
          case validType.Password:
            regex_text = regex.Password;
            break;
          default:
            break;
        }
        const reg = new RegExp(regex_text); // 정규식 test용 객체 생성
        arguments[parameterIndex].status = reg.test(
          arguments[parameterIndex].value
        ); //정규식 성공 여부를 해당 아이템 status에 설정
      }
    }
    return method!.apply(this, arguments); //함수에 단일 배열 전달~
  };
}
파라미터 데코레이터
- 파라미터에 연결함
 
- existingCapitalParameters에
 
- 메타데이터에 배열이 존재한다면 해당 배열
 - 없다면 빈 배열
 
- 위에서 가져온 배열에 매개변수 인덱스 담아주기
 
- metadata 정의
 
function parameterDecorator(
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  let existingCapitalParameters: number[] =
    Reflect.getOwnMetadata(checkCapitalMetadataKey, target, propertyKey) || [];
  existingCapitalParameters.push(parameterIndex); //metadata에서 가져온 배열에 해당 매개변수 인덱스를 담아주기
  Reflect.defineMetadata(
    checkCapitalMetadataKey,
    existingCapitalParameters,
    target,
    propertyKey
  );
  // 데코레이터 내부에 metadata 정의
}
적용 부분
  @validateDecorator
  checkValidate(@parameterDecorator item: validItem): void {
    console.log(item);
    // {
    //     "id": 4,
    //     "type": "Email",
    //     "value": "fdsafd@fdsafd.com",
    //     "status": true,
    //     "errMsg": "이메일 형식 (ex : email@email.com)"
    // }
  }'코딩' 카테고리의 다른 글
| Typescript - 이미지를 Base64로 인코딩 (0) | 2023.03.27 | 
|---|---|
| Python Fast Api (jwt 예제) (0) | 2023.03.22 | 
| CSS Viewport-fit과 Safe Area: 모바일 웹 디자인에서 중요한 역할을 하는 두 속성 (0) | 2023.03.21 | 
| WebRTC (2) - 구글 예제 챕터 2 풀어보기 (0) | 2023.01.26 | 
| WebRTC (0) | 2022.10.06 |