TypeScript 강의 정리: 데코레이터

zeroequaltwo·2022년 10월 11일
0

TS

목록 보기
8/8
post-thumbnail

1. 데코레이터란?

1) 메타 프로그래밍이란?

  • 메타프로그래밍이란 자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며 프로그램을 작성·수정하는 것을 말한다.(from 위키백과)
  • 클래스와 데코레이터를 작성해두면 end user가 이를 유틸리티처럼 사용하며 손쉽게 개발할 수 있게 된다. 이런 의미에서 데코레이터를 메타 프로그래밍을 가능하게 한다고 하는 모양이다...?(제대로 이해했는지 몰것음)

2) 데코레이터란?

  • 클래스 선언과 멤버에 대한 주석(annotations)과 메타 프로그래밍 구문을 모두 추가 할 수있는 방법을 제공하는 기능으로, 데코레이터 함수는 인자로 전달받은 함수의 동작을 수정한다.
  • 매개변수(parameter) & 속성(property) & 접근자(accessor) & 메소드 & 클래스에 첨부할 수 있는 특별한 선언 함수

3) 데코레이터를 사용하는 이유

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

4) 데코레이터의 실행시점

  • 데코레이터는 JS에서 클래스 및 constructor 함수만 정의되면 인스턴스화하지 않아도 실행된다.

5) 데코레이터 작성하기

  • tsconfig.json 파일에서 컴파일러 옵션 중 experimentalDecorators를 true값으로 활성화해야 원활하게 사용 가능하다.
  • 데코레이터 함수를 사용할 때는 데코레이터를 사용할 대상 윗줄에 @ + 데코레이터 이름을 작성한다.
// 클래스 데코레이터 예시
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. 데코레이터 팩토리

  • 데코레이터 팩토리 함수는 데코레이터 함수를 감싸는 wrapper 함수로, 보통 데코레이터 함수가 사용자로부터 인자를 전달 받을 수 있도록 설정할 때 사용한다.
function WithTemplate(template: string, hookId: string) {  // wrapper
  return function (_: Function) {  // 인자가 있어야됨을 알지만 실제로 사용하진 않은 언더바(_)를 사용한다.
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  };
}

@WithTemplate("<h1>My Person Object</h1>", "app")  // 인자를 받아야 하기 때문에 () 추가
class Person {
  name = "Max";

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

const pers = new Person();

▽▽▽ 결과 화면 ▽▽▽


3. 멀티 데코레이터

  • 데코레이터는 여러개 적용이 가능한데 각 데코레이터 간의 실행 순서에 유의하여 작성할 필요가 있다.
  • 각 데코레이터 표현식은 위에서 아래 방향(⬇︎)으로 평가 + 실행 결과는 아래에서 위로(⬆︎) 함수를 호출한다.
function Logger(logString: string) {
  console.log("Logger factory");  // 첫번째
  return function (_: Function) {
    console.log(logString);  // 네번째
  };
}

function WithTemplate(template: string) {
  console.log("template factory");  // 두번째
  return function (_: Function) {
    console.log(template);  // 세번째
  };
}

@Logger("LOGGING")
@WithTemplate("Rendering Template")
class Person {
  name = "Max";

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

const pers = new Person();
// Logger factory
// template factory
// Rendering Template
// LOGGING
// Creating person object...

4. 데코레이터 종류

  • 데코레이터가 어디에 장식되느냐에 따라서, 데코레이터 함수로 넘어오는 인자의 개수나 리턴 구성이 달라지게 된다.

1) 클래스 데코레이터

  • 인자: constructor -> TS가 클래스를 실행할 때 클래스의 constructor를 데코레이터의 파라미터로 자동 전달하므로, 생정자를 명시적으로 전달하지 않아도 된다.
  • 리턴값: class, void
// class를 반환하는 예시
function WithTemplate(template: string, hookId: string) {
  return function <T extends { new (...args: any[]): { name: string } }>(
    originalConstructor: T
  ) {
    // 기존의 constructor를 유지한 채로 새로운 contructor를 반환한다.
    return class extends originalConstructor {
      constructor(..._: any[]) {
        super();  // 원본 훼손 X
        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("Creating person object...");
  }
}

const pers = new Person();

▽▽▽ 결과 화면 ▽▽▽

2) 프로퍼티 데코레이터

  • 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  • 두번째 인자: 해당 property의 이름
  • 리턴값은 무시됨
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 price - should be positive!");
    }
  }

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

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

3) 접근자/메소드 데코레이터

  • 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  • 두번째 인자: 해당 property/method의 이름
  • 세번째 인자: 해당 property/method의 descriptor
  • 리턴값: 새로운 Property Descriptor, void
// Accessor decorator(getter/setter에 적용되는 데코레이터)
function Log(target: any, name: String, descriptor: PropertyDescriptor) {
  console.log("Accessor decorator!");
  console.log(target);
  console.log(name);
  console.log(descriptor);
}

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

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

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

  getPriceWithTax(tax: number) {
    return this._price * (1 + tax);
  }
}
// Method decorator
function Log(target: any, name: String | Symbol, descriptor: PropertyDescriptor) {
  console.log("Method decorator!");
  console.log(target);
  console.log(name);
  console.log(descriptor);
}

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

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

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

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

4) 매개변수 데코레이터

  • 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체
  • 두번째 인자: 매개변수가 들어있는 메소드의 이름
  • 세번째 인자: 메소드 내 파라미터의 index(0부터 시작)
  • 리턴값은 무시됨
function Log(target: any, name: String | Symbol, position: number) {
  console.log("Parameter decorator!");
  console.log(target);
  console.log(name);
  console.log(position);
}

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

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

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

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

5. Autobind 데코레이터 작성해보기

  • 아래와 같이 작성하면 버튼을 클릭했을 때 undefined만 뜨는데, addEventListener가 실행되어야 하는 함수 안에 있는 this를 이벤트의 대상과 바인딩하기 때문이다.
class Printer {
  message = "This works!";

  showMessage() {
    console.log(this.message);
  }
}

const p = new Printer();

const button = document.querySelector("button")!;
button.addEventListener("click", p.showMessage); 

▽▽▽

  • 메소드 데코레이터를 통해 기존의 메소드를 대체해 해당 메소드가 지칭하는 this의 값이 항상 인스턴스를 향하도록 조정
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjustedDescriptor: PropertyDescriptor = {
    configurable: true,
    enumerable: false,
    get() {
      const boundFunction = originalMethod.bind(this); // 여기서 this는 getter를 트리거하는 모든 것을 참조한다.
      return boundFunction;
    },
  };
  return adjustedDescriptor;
}

class Printer {
  message = "This works!";

  @Autobind
  showMessage() {
    console.log(this.message);
  }
}

const p = new Printer();

const button = document.querySelector("button")!;
button.addEventListener("click", p.showMessage);  // This works!

6. 유효성 검사 데코레이터 작성해보기

interface ValidatorConfig {
  [property: string]: {
    [validatableProp: string]: string[]; // ["required", "positive"]
  };
}

const registeredValidators: ValidatorConfig = {};

function Required(target: any, propName: string) {
  registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
    [propName]: [
      ...(registeredValidators[target.constructor.name]?.[propName] ?? []),
      "required",
    ],
  };
}
function PositiveNumber(target: any, propName: string) {
  registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
    [propName]: [
      ...(registeredValidators[target.constructor.name]?.[propName] ?? []),
      "positive",
    ],
  };
}
function validate(obj: any) {
  const objValidatorConfig = registeredValidators[obj.constructor.name];
  if (!objValidatorConfig) {
    return true;
  }
  let isValid = true;
  for (const prop in objValidatorConfig) {
    for (const validator of objValidatorConfig[prop]) {
      switch (validator) {
        case "required":
          isValid = isValid && !!obj[prop];
          break;
        case "positive":
          isValid = isValid && obj[prop] > 0;
          break;
      }
    }
  }
  return isValid;
}

class Course {
  @Required
  title: String;
  @PositiveNumber
  price: number;

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

const courseForm = document.querySelector("form")!;
courseForm.addEventListener("submit", (event) => {
  event.preventDefault();
  const titleEl = document.getElementById("title") as HTMLInputElement;
  const priceEl = document.getElementById("price") as HTMLInputElement;

  const title = titleEl.value;
  const price = +priceEl.value;

  const createdCourse = new Course(title, price);
  if (!validate(createdCourse)) {
    alert("Invalid input, please try again!");
    return;
  }
  console.log(createdCourse);
});

참고: 데코레이터
참고: 데코레이터2

profile
나로 인해 0=2가 성립한다.

0개의 댓글