article

Typescript에서 Decorator 사용하기

kemut 2023. 3. 19. 17:32

TS에서 Decorator의 종류

  • 타입스크립트의 데코레이터는 해당 데코레이터가 어느 위치에 있냐에 따라 구현체가 달라진다.
  • 일반 함수에서는 사용할 수 없으며, class로 선언해야 사용할 수 있다.
  • 타입스크립트의 데코레이터는 문법적 설탕이다. 고차함수 / Proxy / Reflect 로 구현되어있다.

 

method decorator

  • class의 method에 사용하는 데코레이터
  • 메서드의 인자, 결과값 등을 받아 다양한 처리를 할 수 있다
  • parameter → originalMethod, context
    • originalMethod : 해당 클래스에 대한 모든 메서드. 예시에선 { construct:.., style:.., js:.., html:.. } 가 된다.
    • context : 데코레이터를 사용한 함수 이름을 말한다. 예시에선, html이 contenxt가 된다.

return한 html string을 document에 그려주는 데코레이터

// Main.ts
import render from '../../decorator/render'

class Main {
  private style() {}

  private js() {}

  @render
  public html() {
    const template = `<div>메인페이지입니다.</div>`
    return template
  }
}

const mainPage = new Main()
export default mainPage
// render.decorator.ts
export default function render(originalMethod: any, context: any) {
  const body = document.querySelector('body')
  const template = originalMethod[context]()
  body!.insertAdjacentHTML('beforeend', template)
}

 

공식문서에 나온 로깅하는 데코레이터

// log.decorator.ts
function loggedMethod(originalMethod: any, _context: any) {

    function replacementMethod(this: any, ...args: any[]) {
        console.log("LOG: Entering method.")
        const result = originalMethod.call(this, ...args);
        console.log("LOG: Exiting method.")
        return result;
    }

    return replacementMethod;
}

loggedMethod가 새 함수를 반환했기 때문에 이 함수가 원래 greet의 정의를 대채했다.

  1. 콘솔에 "LOG: Entering method."
  2. 모든 인수를 원래 메서드(greet)에 전달하고 실행한다.
  3. 콘솔에 "Exiting..."
  4. 원래 메서드(greet)의 결과를 return 한다.

 

Class Decorator

  • 클래스에 속성을 추가하여 반환한다.
// Main.ts
import ComponentBaseEntity from '../../decorator/ComponentBaseEntity.decorator'
import render from '../../decorator/render.decorator'

@ComponentBaseEntity('page-main')
class Main {
  constructor() {}
  ..
}

const mainPage = new Main()
export default mainPage
// ComponentBaseEntity.decorator.ts
export default function ComponentBaseEntity(id: string) {
  return function (targetClass: any) {
    targetClass.prototype.id = id
  }
}
  • 이제 mainaPage.id는 main-page 가 된다.
  • 🚨 class decorator에서 삽입한 속성은 타입 추론에서 찾을 수 없다..

 

Property decorators

  • 속성 데코레이터는 특정 이름의 속성이 클래스에 대해 선언되었는지 확인하는 데에만 사용할 수 있다.
  • 필드 값에 액세스할 수 없으며 변경할 수 없다.
// Main.ts
import ComponentBaseEntity from '../../decorator/ComponentBaseEntity.decorator'
import propertyDecorator from '../../decorator/property.decorator'
import render from '../../decorator/render.decorator'

class Main {
  constructor() {
    this.articleList = ['첫번째글', '두번째글', '세번째글']
  }

  @propertyDecorator
  static someStatic = 'static and static'
  @propertyDecorator
  articleList: string[]
  ...
}

const mainPage = new Main()
export default mainPage
// property.decorator.ts
export default function propertyDecorator(targetClass: any, propertyKey: any) {
  console.log('1️⃣', targetClass)
  console.log('2️⃣', propertyKey)
}
  • targetClass
    • property가 static일 경우 → class.prototype이 된다.
    • static이 아닐 경우 → class가 된다.
  • propertyKey
    • 값에는 접근할 수 없으며, keyName만 출력된다.

결과

// Reflect.defineMetadata로 민감한 정보 숨기는 공식문서 예제
import 'reflect-metadata'

function sensitive(firstArgument, propertyName) {
  Reflect.defineMetadata(`sensitive:${propertyName}`, true, firstArgument)
}
class User {
  private email: string
  private username: string
  @sensitive
  private password: string
  @sensitive
  private cardNumber: string
  constructor(email: string, username: string, password: string, cardNumber: string) {
    this.email = email
    this.username = username
    this.password = password
    this.cardNumber = cardNumber
  }
  toString() {
    let userJSON = {}
    for (const key of Object.keys(this)) {
      const isSensetiveField = Reflect.getMetadata(`sensitive:${key}`, this)
      if (!isSensetiveField) {
        userJSON[key] = this[key]
      }
    }
    return JSON.stringify(userJSON)
  }
}
const person1 = new User('p.shaddel@gmail.com', 'pshaddel', 'mySecretPassword', '12345678')
const person2 = new User('john@gmail.com', 'johnUserName', 'JohnSecretPassword', '87654321')
console.log('Person1:', person1.toString())
console.log('Person2:', person2.toString())

 

Accessor Decorators

  • get과 set을 실행하면 직전에 해당 데코레이터 내에 행동이 실행되어 리턴된다.
  • Proxy처럼 동작한다.
// Main.ts
class Main {
  constructor() {
    this._articleList = ['first thing..', 'second thing..', 'third thing..']
  }

  private _articleList: string[]

  @accessor
  get articleList() {
    return this._articleList
  }
  set articleList(newList: string[]) {
    this._articleList = newList
  }

  ...
}

const mainPage = new Main()
export default mainPage
// accessor.decorator.ts
import checkXSS from '../util/checkXSS'

export default function accessor(targetClass: any, methodName: any, descriptor: any) {
  return {
    get: function () {
      // 첫번째 글을 대문자로 바꿔서 보여주기
      const transform = descriptor.get.call(this).map((article: string) => article.charAt(0).toUpperCase() + article.slice(1))
      return transform
    },
    set: function (newList: string[]) {
      // 보안상 문제가 있는 문자가 있는지 확인하는 로직
      descriptor.set.call(
        this,
        newList.map((item: string) => checkXSS(item))
      )
    },
  }
}
// 실행 결과
console.log(mainPage.articleList) 
// ['First thing..', 'Second thing..', 'Third thing..']

mainPage.articleList = ['<script>alert("hi hello")</script>']
console.log(mainPage.articleList) 
// ['Scriptalerthi hello/script']

 

 

 

Announcing TypeScript 5.0 - TypeScript

getting-started-with-typescript/decorators at master · course-zero/getting-started-with-typescript

https://ui.toast.com/posts/ko_20210413

How To Create a Custom Typescript Decorator

Start Implementing Your Own Typescript Property Decorators