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의 정의를 대채했다.
- 콘솔에 "LOG: Entering method."
- 모든 인수를 원래 메서드(greet)에 전달하고 실행한다.
- 콘솔에 "Exiting..."
- 원래 메서드(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
'article' 카테고리의 다른 글
Next13 - Component & CSS (0) | 2023.04.12 |
---|---|
React에서 SSR이 필수일까요? (0) | 2023.04.03 |
(번역) Everything you need to know about Concurrent React (with a little bit of Suspense) (0) | 2023.03.05 |
(번역) Why everyone is talking about Astro and island Architecture? 🚀 (0) | 2023.03.03 |
(번역) Intro to HTML-first Frontend Frameworks (0) | 2023.03.03 |