TypeScript Decorator

강정우·2023년 6월 18일
0

TypeScript

목록 보기
21/23
post-thumbnail

Decorator

  • decorator는 메타 프로그래밍에서 매우 유용하다. 그럼 메타 프로그래밍이란 무엇일까? => 소프트웨어 개발 생산성을 향상시키고 코드의 유지 보수성을 향상시킬 수 있는 강력한 도구이다. 즉, 사용자에겐 보여지지 않으며(물론 상황에 따라 보여지게 할 수 있지만) 타 개발자와함께 협업할 때 빛을 발하는 방식이다.

  • 데코레이터를 사용하면 일반적으로 작업하지 않지만 대신 예를 들어 클래스 중 하나 또는 클래스의 메서드가 올바르게 사용되록 보장하거나 숨겨진 변환 등을 수행할 수 있다.

  • 참고로 decorator는 class와 관련이 있다. 또한 선언할 때 class와 같이 대문자로 선언해주는 것이 좋다.

요건

  • decorator를 사용하기 위한 최소 요구사항은
    JS 버전은 최소 es6^ 이어야한다. experimentalDecorators 설정을 true로 만들어줘야한다.

@

  • at 이라고 부르며 TS가 인식하는 특수 식별자이다.

  • 또한 decorator를 pointing을 해야하며 실행을 해선 안 된다.

  • decorator는 반드시 인자를 받는다.

  • 데코레이터는 클래스가 정의될 때 실행된다. 인스턴스를 뽑아낼때 실행되는 것이 아니다.

function Logger(target: Function) {
    console.log('logging...');
    console.log(target);
}

@Logger
class Person{
    name = '정우';
    constructor() {
        console.log('사람 객체를 만드는 중입니다...')
    }

    getLog() {
        console.log('get Log');
    }
}

const pers = new Person();

console.log(pers);

decorator factory

function Logger(logString: string) {
    return function (target: Function) {
        console.log(logString);
        console.log(target);
    };
}

@Logger('Logging - Person')
class Person{
    name = '정우';
    constructor() {
        console.log('사람 객체를 만드는 중입니다...')
    }

    getLog() {
        console.log('get Log');
    }
}

const pers = new Person();

console.log(pers);
  • 데코레이터 함수 내부에 값을 전달하는 방법으로
    데코레이터 팩토리를 사용하면 데코레이터가 내부적으로 수행하는 작업을 구성할 수 있는 더 많은 기능과 가능성을 얻을 수 있다.

라이브러리화 하기

  • 또한 미리 작성해두면 어디서든 import하여 사용하는 것이 가능하다. 아래는 html 태그를 입력하고 위치를 지정할 id값을 입력하면 해당 위치에 작성한 html 코드를 추가해주는 decorator이다.
function WithTemplate(template: string, hookId: string) {
    return function (constructor:any) {
        const hookEl = document.getElementById(hookId);
        const p = new constructor();
        if (hookEl) {
            hookEl.innerHTML = template;
            hookEl.querySelector('h1')!.textContent = p.name;
        }
    }
}

// @Logger('Logging - Person')
@WithTemplate('<h1>My person Obj</h1>','app')
class Person{
    name = '정우';
    constructor() {
        console.log('사람 객체를 만드는 중입니다...')
    }

    getLog() {
        console.log('get Log');
    }
}

실행순서

function Logger(logString: string) {
    return function (target: Function) {
        console.log(logString);
        console.log(target);
    };
}

function WithTemplate(template: string, hookId: string) {
    return function (constructor:any) {
        const hookEl = document.getElementById(hookId);
        const p = new constructor();
        if (hookEl) {
            hookEl.innerHTML = template;
            hookEl.querySelector('h1')!.textContent = p.name;
        }
    }
}

@Logger('Logging - Person')
@WithTemplate('<h1>My person Obj</h1>','app')
class Person{
    name = '정우';
    constructor() {
        console.log('사람 객체를 만드는 중입니다...')
    }

    getLog() {
        console.log('get Log');
    }
}
  • 이런식으로 2개의 decorator가 있다면 어떻게 실행될까?
  1. decorator facotry를 먼저 실행한다. Logger => WithTemplate
  2. class를 만나서 execute되는 decorator는 bottom-up 순서대로 실행된다. WithTemplate => Logger
  3. class의 인스턴스를 생성할 때 실행되는 것이 아닌 class가 정의될 때 1번 실행된다.

class의 속성으로 사용하기

  • 이때는 class에 사용한다는 constructor가 필요없다. 즉, decorator factory가 필요없다는 뜻이다.
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('값이 이상합')
        }
    }
    constructor(t: string, p: number) {
        this.title = t;
        this._price = p;
    }
    getPriceWithText(tax: number) {
        return this._price * (1 + tax);
    }
}

accessor

property accessor

  • 3개의 매개변수를 갖는다. 1. target, 2. name, 3. descriptor

1.은 해당 accessor가 가르키는 객체이다. 이때 TS는 해당 descorator(accessor)가 어디에 쓰일지 모르기 때문에 이때는 type이 any여야 맞다.

2.name은 accorr의 이름이다. 3.의 descriptor는 말 그대로 accessor의 속성을 좀 더 디테일하게 설명해주는 것이다.

function Log2(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;
    @Log2
    set price(val: number) {
        if (val > 0) {
            this._price = val;
        } else {
            throw new Error('값이 이상합')
        }
    }
    constructor(t: string, p: number) {
        this.title = t;
        this._price = p;
    }
    getPriceWithText(tax: number) {
        return this._price * (1 + tax);
    }
}

function accessor

function Log3(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('값이 이상합')
        }
    }
    @Log3
    constructor(t: string, p: number) {
        this.title = t;
        this._price = p;
    }
    getPriceWithText(tax: number) {
        return this._price * (1 + tax);
    }
}

argument accessor

  • 파라미터 데코레이터도 3개의 매개변수를 받는다. 다른점은 마지막 면수가 descriptor가 아닌 매개변수의 순서를 나타내는 position이 들어간다.
function Log4(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('값이 이상합')
        }
    }
    constructor(t: string, p: number) {
        this.title = t;
        this._price = p;
    }
    getPriceWithText(@Log4 tax: number) {
        return this._price * (1 + tax);
    }
}

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글