Jieunny의 블로그

Section8. 데코레이터 본문

CodeStates/TS 스터디

Section8. 데코레이터

Jieunny 2023. 3. 3. 14:07

📣  강의 내용 정리

✔️ 데코레이터란?

➰ 클래스 선언과 멤버에 대한 주석과 메타 프로그래밍 구문을 모두 추가할 수 있는 방법을 제공하는 기능으로, 데코레이터 함수는 인자로 전달받은 함수의 동작을 수정한다.

➰ 쉽게 말해 데코레이터 패턴은 클래스를 수정하지 않고 클래스의 멤버들의 정의를 수정 및 기능을 확장할 수 있는 구조적 패턴의 하나

➰ 매개변수, 속성, 접근자, 메소드, 클래스에 첨부할 수 있는 특별한 선언 함수

데코레이터가 어디에 장식되느냐에 따라서, 데코레이터 함수로 넘어오는 인자의 개수나 리턴값이 달라진다

(데코레이터의 종류가 달라지는 것)

 

✚ 메타프로그래밍이란?

더보기

➰ 자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며 프로그램을 작성, 수정하는 것을 말한다.

📚 참고 : https://velog.io/@zeroequaltwo/TypeScript-%EA%B0%95%EC%9D%98-%EC%A0%95%EB%A6%AC-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0

 

𝟭.  클래스 데코레이터

function Logger(constructor: Function) {
    console.log("Logging...");
    console.log(constructor);
}

@Logger
calss Person {
    name = 'Max';
    
    constructor() {
        console.log('Creating person object...');
    }
}

const pers = new Person();
console.log(pers);

//Logging...
//Class Person {
//    ...
// }
//Creating person object...
//Person {name:'Max'}

➰ 첫문자는 대문자, 소문자 모두 가능하지만 보통 대문자로 많이 쓴다.

➰ 클래스 데코레이터를 이용하면 클래스에서 정의하지 않은 속성을 정의할 수도, 이미 클래스에서 부여한 값을 바꿀 수도 있다.

➰ 데코레이터는 실체화되기 전 클래스가 정의만 되도 실행된다.

클래스 데코레이터의 인자는 constructor(TS가 클래스를 실행할 때 클래스의 constructor를 데코레이터의 파라미터로 자동 전달하므로 명시적으로 전달하지 않아도 된다)

리턴값은 class나 void 이다.

 

𝟮.  데코레이터 팩토리 작업하기

✔️ 데코레이터를 만드는 대신, 데코레이터 팩토리(factory)를 정의할 수 있다.

✔️ 데코레이터 팩토리 함수 : 데코레이터를 감싸는 wrapper 함수로, 보통 데코레이터 함수가 사용자로부터 인자를 전달받을 수 있도록 설정 할 때 사용한다.

function Logger(logString: string) {     // 팩토리 함수
	return function(constructor: Function) { // 실제 데코레이터
            console.log("Logging...");
    	    console.log(constructor);
    }
}

@Logger('LOGGING - PERSON')
calss Person {
    name = 'Max';
    
    constructor() {
        console.log('Creating person object...');
    }
}

➰ @Logger 부분을 데코레이터 함수 대신 사용할 수 있다 -> 기본 로깅 문자 대신 내가 전달한 문자열을 보여준다.

➰ 팩토리 함수와 함께 실행된다면 데코레이션 함수가 사용하는 값을 커스터마이즈 할 수 있다.

 

𝟯.  더 유용한 데코레이터 만들기

function WithTemplate(template: string, hookId: string) {
    return function(_: Function) {
    // _ : 존재는 알지만 입력하지 않겠다고 명시하는 것
        const hookEl = document.getElementById('hookId');
        if(hookEl){
            hookEl.innerHTML = template;
        }
    }
}

@WithTemplate('<h1>My Person Object</h1>', 'app') 
// HTML 파일에 <div id="app"></div> 선언해준 상태

➰ 화면에 <h1> 속성으로 My Person Object 문자열이 나타난다. 

 

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;
        }
    }
}

@WithTemplate('<h1>My Person Object</h1>', 'app') 
// HTML 파일에 <div id="app"></div> 선언해준 상태
calss Person {
    name = 'Max';
    
    constructor() {
        console.log('Creating person object...');
    }
}

const pers = new Person();
console.log(pers);

➰ 화면에 <h1> 속성으로 Max 가 나타난다.

 

𝟰.  데코레이터 추가하기

✔️ 데코레이터가 나타나는 순서

➰ 하나의 멤버에 동시에 여러 개의 데코레이터를 장식할 수 있다.

➰ 각 데코레이터의 팩토리 함수가 먼저 실행되고(결과는 위에서 아래로), 실제 데코레이터 함수 나중에 (결과는 아래에서 위로)

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")

 

𝟱.  속성(프로퍼티) 데코레이터

function writable(isWritable: boolean) {
  return function (target: any, propertyName: any): any {
    return {
      writable,
    };
  };
}

class Test {
  property = 'property';

  @writable(true)
  public data1 = 0;

  @writable(false)
  public data2 = 0;
}

const t = new Test();
t.data1 = 1000;
t.data2 = 1000; // 런타임 에러 !! - data2는 writable이 false라서 값을 대입할 수가 없다.

➰ 프로퍼티의 설정을 바꿀 수 있다.

➰ 프로퍼티 데코레이터의 첫번째 인자는 static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 프로토타입 객체

➰ 두번째 인자는 해당 프로퍼티의 이름

➰ getter, setter를 설정해서 값을 받아올 수도, 설정할 수도 있다.

 

𝟲.  접근자 & 매개변수 데코레이터(??)

✔️ 접근자 데코레이터 : 접근자 앞에 사용하며 접근자의 속성 설명자에 적용되고 접근자의 정의를 읽거나 수정할 수 있다

➰ 접근자 : 객체 프로퍼티를 객체 외에서 읽고 쓸 수 있는 함수(getter, setter)

➰ 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체

 두번째 인자: 해당 property/method의 이름

➰ 세번째 인자: 해당 property/method의 descriptor

➰ 리턴값: 새로운 Property Descriptor, void

function Enumerable(enumerable: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = enumerable;
  };
}

class Person {
  constructor(private name: string) {}
  
  @Enumberable(true)
  get getName() {
    return this.name;
  }
  
  @Enumberable(false)
  set setName() {
    this.name = name;
  }
}

const person = new Person('hello');
for (let key in person) {
  console.log(`${key}: ${person[key]}`);
}
//결과를 출력하면 getName은 출력되지만 setName은 열거하지 못하게 되었기 때문에 for 문에서 key로 받을 수 없다.

 

✔️ 매개변수 데코레이터 : 메서드의 매개 변수에 선언되며 클래스 생성자 함수나 메서드의 매개변수로 사용할 수 있다.

➰ 첫번째 인자: static 프로퍼티라면 클래스의 생성자 함수, 인스턴스 프로퍼티라면 클래스의 prototype 객체

➰ 두번째 인자: 매개변수가 들어있는 메소드의 이름

➰ 세번째 인자: 메소드 내 파라미터의 index(0부터 시작)

➰ 리턴값은 무시된다.

// Log 매개변수 데코레이터
function Log(t:any, p:string, i:number) {
  console.log(t.name);
  console.log(`
    매개변수 순서: ${i}, 
    멤버 이름: ${p === undefined ? 'constructor' : p}
  `);
}
​
// Button 클래스
class Button {

  el:HTMLButtonElement;
  color:string;

  // 생성자 함수 내에 Log 데코레이터 사용
  constructor(
    @Log el:HTMLButtonElement, 
    @Log color:string = 'transparent'
  ) {
    this.el = el;
    this.color = color;
  }
  
  // 스태틱 메서드 내에 Log 데코레이터 사용
  static initialize(
    @Log els:NodeList, 
    @Log color:string = 'transparent'
  ){
    return [].slice.call(els).map(el => new Button(el, color));
  }

}

// Button.initialize() 스태틱 메서드 사용
const btns = Button.initialize( document.querySelectorAll('.button'), '#900' );

//console
Button
매개변수 순서: 1, 멤버 이름: initialize

Button
매개변수 순서: 0, 멤버 이름: initialize

Button
매개변수 순서: 1, 멤버 이름: constructor

Button
매개변수 순서: 0, 멤버 이름: constructor

전달 받은 인자 중 마지막 매개변수 인덱스는 클래스 생성자 또는 메서드 매개변수 내 데코레이터를 사용한 순서

➰ 매개변수의 순서가 역순이다.

📚 https://typescript-kr.github.io/pages/decorators.html

 

𝟳.  데코레이터는 언제 실행하는가

✔️ 프로퍼티, 메소더, 액세서, 매개 변수 등 어떤 데코레이터든 클래스를 정의할 때 실행된다.

➰ 데코레이터는 클래스가 정의될 때 뒤에서 부가적인 작업을 하는 것

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

 

𝟴. 데코레이터 반환 타입

✔️ 클래스 데코레이터 : 새 함수, 컨스트럭터 함수, 새 클래스를 반환할 수 있다.

✔️ 프로퍼티 데코레이터 : 리턴값은 무시된다.

✔️ 접근자 & 메서드 데코레이터 : 새로운 Property Descriptor, 함수를 반환할 수 있다.

✔️ 매개변수 데코레이터 : 리턴값은 무시된다.

 

𝟵. "Autobind" 데코레이터 만들기

✔️ Autobind 데코레이터 : this를 자동으로 주변의 클래스에 바인딩 해준다.

//index.html
<button> Click me </button>

 

✔️ 직접 바인딩 해주는 경우

//app.ts
class Printer {
    message = 'This works!';
    
    showMessage() {
        console.log(this.message);
    }
}

const p = new Printer();

p.showMessage(); // this가 Printer를 참조
const button = document.querySelector('button')!;
button.addEventListener('click', p.showMessage); // this가 이벤트의 대상을 참조
// event가 발생했을 때 this가 참조하고 있는 값이 showMessage함수가 참조하고 있는 값과 다르기 때문이 실행되지 않는다.

button.addEventListener('click', p.showMessage.bind(p));
//showMessage 대신 p를 참조하여 다시 Printer를 참조하게 만든다.

 

✔️ Autobind 데코레이터 사용

function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) { // 데코레이터
    const originalMethod = descriptor.value;
    const adjDescriptor = { // 나중에 반환될 값
        configurable: true,
        enumerable: false, // for in 루프 표시 안함.
        get() {
            const boundFn = originalMethod.bind(this);
            return boundFn;
        }	// getter 계층 추가하기 
        // 여기서 this는 this가 속해 있는 확실한 객체(원래 우리가 정의한 메서드에 있는 객체를 참조한다.
    }
    return adjDescriptor;
}
// 새로운 디스크립터 객체를 반환하면, 그 디스크립터 객체가 이전의 디스크립터를 덮어쓴다.
// -> 이전의 매서드 디스크립터를 대체한다.
// -> 이전의 메서드 구성을 새로운 구성으로 대체하고(get()), 그러면 getter계층이 추가되는 것이다.

class Printer {
    message = 'This works!';
    
    @Autobind
    showMessage() {
        console.log(this.message);
    }
}

class p = new Printer();
const button = document.quertSelector('button')!;
button.addEventListener('click', p.showMessage);
// 이제 직접 바인딩 하지 않아도 작동한다!

➰ 메소드 데코레이터를 통해 기존의 메소드를 대체해 해당 메소드가 지칭하는 this의 값이 항상 인스턴스를 향하도록 조정

 

𝟭𝟬. 유효성 검사 데코레이터

//index.html
<form>
    <input type="text" placeholder="Course title" id="title">
    <input type="text" placeholder="Course price" id="price">
    <button type="submit">Save</button>
</form>
// app.ts

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],
        // registeredValidators에 있는 해당 클래스 이름의 기존 키-값 쌍을 가져온다.
        [propName]: [...registeredValidators[target.constructor.name]?.[propName] ?? []), 'required']
    };
    
}
// 데코레이터

function PositiveNumber(target: any, propName: string) {
    registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
        [propName]: [propName]: [...registeredValidators[target.constructor.name]?.[propName] ?? []), 'positive']
    };
}
// 데코레이터

function validate(obj: object) {
    const objValidatorConfig = registeredValidators[obj.constructor.name];
    if(!objValidatorConfig) {
    // 분석 할 객체가 없을 때
        return true;
    }
    let isValid = true; // 모든 프로퍼티가 유효성 검증이 되었는지 표시하는 변수
    for(const prop in objValidatorConfig){
        console.log(prop);
    // 프로퍼티에 대해 있을 수 있는 모든 validator를 검사한다.
        for(const validator of objValidatorConfig[prop]{
        // positive 나 required 같은 것들을 가져오게 되고, 이들은 모두 validator 상수에 저장된 것들.
            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;
    // + 붙이면 Number로 전환됨.
    
    const createdCourse = new Course(title, price);
    
    if(!validate(createdCourse)) {
        alert('Invalid input, please try again!');
        return;
    }
    console.log(createdCourse);
 });

💡  새로 알게 된 점

📚 데코레이터를 잘 정리해 놓은 블로그 : https://velog.io/@zeroequaltwo/TypeScript-%EA%B0%95%EC%9D%98-%EC%A0%95%EB%A6%AC-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0

 

1. 데코레이터가 어떤 것인지, 어떤 종류가 있는지, 왜 사용하는지

2. 근데 너무 복잡해서 아직 이걸 사용하는게 정말 편리한지 모르겠음..

'CodeStates > TS 스터디' 카테고리의 다른 글

Section10. 모듈 및 네임스페이스  (0) 2023.03.23
Section9. Drag & Drop 프로젝트 만들기  (2) 2023.03.20
Section 7. 제네릭  (0) 2023.02.22
Section 6. 고급 타입  (0) 2023.02.21
Section 5. 클래스 & 인터페이스  (0) 2023.02.21