- Published on
데코레이터
데코레이터
클래스 데코레이터 예시
tsconfig.json
{
"compilerOptions": {
//...
"experimentalDecorator": true
}
}
tsconfig.json에서experimentalDecorator값을true로 변경
app.ts
function Logger(constructor: Function) {
console.log('Logging...')
console.log(contructor)
}
@Logger
class Person {
name = 'Max'
constructor() {
console.log('Creating person object...')
}
}
const pers = new Person()
console.log(pers)
- 클래스 앞에
@을 붙여 데코레이터를 더하고 작성한Logger함수를 정의@: 코딩에서 읽히거나 찾게 되는 특별한 식별자 상징
- 데코레이터에서는 인수(
target)를 받으므로Person의contructor함수를 전달받음- 데코레이터는 실체화되기 전 클래스가 정의만 돼도 실행됨
데코레이터 팩토리 작업
function Logger(logString: string) {
return function (constructor: Function) {
console.log(logString)
console.log(contructor)
}
}
@Logger('LOGGING - PERSON')
class Person {
name = 'Max'
constructor() {
console.log('Creating person object...')
}
}
const pers = new Person()
console.log(pers)
- 데코레이터를 위의 예시처럼, 직접 만드는 대신
팩토리를 정의할 수 있음- 어떤 대상에 데코레이터를 할당할 때 설정할 수 있도록 함
return을Logger함수 내부에 입력하고 새 익명 함수를 반환Logger함수가 실행되면 바깥 코드가 실행되고, 유효한 데코레이터 함수이자 내부 코드인 반환 값이 따라 붙음
- 문자열
logString을 통해 함수가 실행될 때, 특정 문자열을 인수로 전달받아 출력할 수 있음- 팩토리 함수와 함께 실행되면 데코레이션 함수가 사용하는 값을 커스터마이징할 수 있음
데코레이터 팩토리 심화
function WithTemplate(template: string, hookId: string) {
return function (constructor: any) {
const hookEl = document.getElementById(hookId)
const p = new contructor()
if (hookEl) {
hookEl.innerHTML = template
hookEl.querySelector('h1')!.textContent = p.name
}
}
}
// @Logger('LOGGING - PERSON')
@WithTemplate('<h1>My Person Object</h1>', 'app')
class Person {
name = 'Max'
constructor() {
console.log('Creating person object...')
}
}
const pers = new Person()
console.log(pers)
- 팩토리 함수와 팩토리 데코레이터를 통해 템플릿과 함께 새 데코레이터 팩토리를 만들 수 있음
- 템플릿을 문자열로 하고, 훅 아이디가 문자열인
WithTemplate함수를 정의하여 내부 함수에 클래스 생성자 함수를 로드하여 템플릿을 렌더할 수 있음 - 데코레이터 함수 생성하면 생성된 데코레이터 함수를 언제든지 불러와서 클래스에 추가할 수 있음
- 타입스크립트를 사용하는 앵귤러 프레임워크도 이와 같이 동작하는데, 데코레이터를 마치 컴포넌트 데코레이터처럼 사용하고, 해당 컴포넌트 템플릿 등을 명시하는 오브젝트를 통과하게 함
다양한 데코레이터 추가
function Logger(logString: string) {
console.log('LOGGER FACTORY');
return function(constructor: Function){
console.log(logString);
console.log(contructor);
};
}
function WithTemplate(template: string, hookId: string){
console.log('TEMPLATE FACTORY');
return function(constructor: any){
console.log('Rendering template');
const hookEl = document.getElementById(hookId);
const p = new contructor();
if (hookEl){
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = p.name;
}
}
}
// @Logger('LOGGING - PERSON')
@Logger('LOGGING')
@WithTemplate('<h1>My Person Object</h1>', 'app')
class Person {
name = 'Max';
constructor() {
console.log('Creating person object...');
}
}
const pers = new Person();
console.log(pers);
- 데코레이터는 데코레이터를 사용할 수 있는 어떤 곳 혹은 클래스에 하나보다 많은 데코레이터를 사용할 수 있음
- 하나 이상의 데코레이터를 추가하면 데코레이터 함수는
bottom-up로 실행됨
속성 데코레이터
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)
}
}
- 클래스 이외에도 다른 곳에 데코레이터를 사용할 수 있는데,
Product클래스 내부에Log데코레이터를 사용해 두 프로퍼티,constructor생성자 함수, 메소드를 Log 함수를 통해 취득할 수 있음 - 첫 인수는 프로퍼티의
타겟을, 두 번째 인수는 프로퍼티의이름을 받는 Log 데코레이터 함수를 정의- 타겟은 객체가 어떤 구조를 가질 지 알 수 없으므로
any타입으로 설정 - 프로퍼티 이름은
문자열혹은심볼타입로 설정
- 타겟은 객체가 어떤 구조를 가질 지 알 수 없으므로
- 객체의 프로토타입과 프로퍼티의 이름, 메소드의 정보를 취득할 수 있음
접근자 & 매개변수 데코레이터
function Log(target: any, propertyName: string | Symbol) {
console.log('Property decorator!');
console.log(target, propertyName);
}
function Log2(
target: any,
name: string,
descriptor: PropertyDescriptor
){
console.log('Accessor decorator!');
console.log(target);
console.log(name);
console.log(descriptor);
})
function Log3(
target: any,
name: string | Symbol,
descriptor: PropertyDescriptor
){
console.log('Method decorator!');
console.log(target);
console.log(name);
console.log(descriptor);
})
function Log4(
target: any,
name: string | Symbol,
position: number
){
console.log('Parameter decorator!');
console.log(target);
console.log(name);
console.log(position);
})
class Product {
@Log
title: string;
private _price: number;
@Log2
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;
}
@Log3
getPriceWithTax(@Log4 tax: number){
return this.price * (1 + tax);
}
}
- 프로퍼티 외에 접근자에 데코레이터를 더할 수 있음
Log2 - 접근자 데코레이터
target- 대상이 되는 프로토 타입의 정보
name- 외부 액세서의 이름(
price)을 출력
- 외부 액세서의 이름(
descriptor- 프로토 타입의 프로퍼티 디스크립터
- 열거되지 않지만 변경할 수 있음
- get 함수를 제외한
set함수 정보 취득
- 프로토 타입의 프로퍼티 디스크립터
Log3 - 메서드 데코레이터
target- 대상이 되는 프로토 타입의 정보
name- 메서드의 이름(
getPriceWithTax)을 출력
- 메서드의 이름(
descriptor- 프로토 타입의 프로퍼티 디스크립터
- 열거되지 않지만 변경할 수 있음
value,writable속성 취득
- 프로토 타입의 프로퍼티 디스크립터
Log4 - 매개변수 데코레이터
target- 대상이 되는 프로토 타입의 정보
name- 매개변수를 사용하는 메서드의 이름(
getPriceWithTax)을 출력
- 매개변수를 사용하는 메서드의 이름(
position- 0부터 시작하는 인수의 수(
index)
- 0부터 시작하는 인수의 수(
클래스 데코레이터에서 클래스 반환
function WithTemplate(template: string, hookId: string){
return function<T extends { new(...args: any[]): {name: string} }>(
originalContstructor: T
){
return class extends originalContstructor {
constructor(..._: any[]) {
super();
const hookEl = document.getElementById(hookId);
if (hookEl){
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = this.name;
}
}
}
}
}
// @Logger('LOGGING - PERSON')
@Logger('LOGGING')
@WithTemplate('<h1>My Person Object</h1>', 'app')
class Person {
name = 'Max';
constructor() {
console.log('Creating person object...');
}
}
const pers = new Person();
console.log(pers);
- 데코레이터 함수는 내부의 함수에서 값을 반환할 수 있음
- 이를 이용해 클래스의 새로운 컨스트럭터 함수를 반환할 수 있는데 기존의 생성자 함수를 대체할 수 있음
- 즉, 새 함수, 컨스트럭터 함수를 반환하거나 새 클래스를 반환할 수 있음
super()- 오리지널 함수와 클래스를 저장
return class extends originalContstructor- 오리지널 클래스를 확장해서 오리지널 클래스에 있던 모든 것을 저장
- 새 커스텀 클래스로 기존의 클래스를 대체하여 추가 로직을 실행
Autobind 데코레이터 만들기
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
const boundFn = originalMethod.bind(this)
return boundFn
},
}
return adjDescriptor
}
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)
button요소에showMessage메서드를 실행하기 위해서는 클래스내에 바인딩되어 있는this키워드의 컨텍스트나 레퍼런스가 호출되었을 때와 동일하지 않으므로bind메서드를 바인딩해야 함this키워드를 메서드가 속해 있는 객체로 설정PropertyDescriptor의 속성을 재정의
데코레이터 유효성 검증
validator가 타입스크립트 데코레이터를 이용하여 작동하는 방식
interface ValidatorConfig {
[property: string]: {
[validatableProp: string] : string[] // ['required', 'positive']
}
}
const registeredValidators: ValidatorConfig = {};
function Required(target: any, propName: string) {
registredValidators[target.contructor.name] = {
...registredValidators[target.contructor.name],
[propName]: ['required']
}
}
function PositiveNumber(target: any, propName: string) {
registredValidators[target.contructor.name] = {
...registredValidators[target.contructor.name],
[propName]: ['positive']
}
}
function validate(obj: any) {
const objValidatorsConfig = registeredValidators[obj.constructor.name];
if(!objValidatorConig){
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.addEventListner('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 inputs, please try again!');
return;
}
}
- 데코레이터의
유효성 검증하는 예시를 확인할 수 있음 - 각각의 데코레이션 함수를 정의하고
Course클래스 내에 데코레이션을(@Required,@PoritiveNumber) 사용
Referenced