it공부 (개념)/javascript

Decorator(데코레이터) 와 Meta-programming(메타프로그래밍): Typescript(타입스크립트) 예제

cantor 2023. 1. 31. 21:25

 

Decorator(데코레이터?)

    • 동사 Decorate의 사전적 정의: 장식하다, 모양내다, 꾸미다
      데코레이터: 장식, 장식하는 사람

 

  • 타입스크립트의 데코레이터:
    클래스와 클래스 멤버들의 기능을 추가 및 번경하는 "함수".



  • 데코레이터의 특징
    1. class를 수정하거나 subclass를 생성하지 않고도 기능을 변형할 수 있다.
    2. 메타프로그래밍적 요소이다.
    3. run-time에 실행된다.

    사실 위의 특징 세 개는 모두 "메타프로그래밍"으로 요약된다.

    메타프로그래밍: 

    사람이 직접 코드를 번경하는 것이 아니라 
    프로그램이 자기 자신 혹은 다른 컴퓨터 프로그램을 데이터로 취급하며
    프로그램을 작성·수정하는 것을 말한다 

    데코레이터에 의해 클래스기능이 변형되는 시점은 run-time이다.
    프로그램이 실행되는 동안에 기능수정이 이루어진다 

    또, 사람이 class를 수정하거나 subclass를 생성하지 않고 데코레이터라는 일종의 
    함수에 의해  클래스가 수정된다. 즉 함수(프로그램)에 의해 클래스(프로그램)가 수정된다.
 

VSCODE에서 TypeScript 개발 준비하기

썸네일 이미지 Microsoft icons created by Freepik - Flaticon 저번에 소개했던 TypeScript의 개발준비를 위한 설정을 해보자. https://batcave.tistory.com/33 TypeScript(타입스크립트)? 간단하게 알아보기 Typescript icons cre

batcave.tistory.com

 

타입스크립트에서 데코레이터 환경 설정하기

 

1. tsconfig.json 준비

 

우선 tsconfig.json 파일을 만들어준다.

 

직접 만들기보다는 watch mode(워치모드)를 활성화시켜서 생성하는 것이 편하다.

터미널에 다음과 같은 코드를 입력하면 워치모드로 전환되며 tsconfig.json파일이 자동으로 생성된다.

tsc -w


// 워치모드를 실행할경우 내가 .ts 타입스크립트 파일을 저장할때
// 언제나 자동으로 .js 자바스크립트로 컴파일된다.


//tsc app.ts 혹은 tsc 등의 키워드로 일일히 컴파일 할 필요가 없어져서 
//매우 편리하다.

 

2. 데코레이터 활성화

 

다음과 같은 코드를 터미널에 입력해 준다.

tsc --target ES5 --experimentalDecorators

 

또는 tsconfig.json 파일의 complierOption 항목의 "experimentalDecorators" 값을 true로 바꿔준다.

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

 

 

데코레이터의 인자

데코레이터는 클래스 및 클래스멤버들을 꾸미는 데 사용된다. 

=(클래스 및 클래스 멤버를 인자로 받아들여서 실행된다)

    1. Class(클래스)                     
    2. Class Method(메서드)        
    3. Class Property(속성)          
    4. Class Method Parameter(파라미터)
    5. Class Accessor(접근자)

 

 

사실 데코레이터를 정의할 때 필요한 실제 인자는 따로 있다.

 

    1. Class(클래스)                                          ->  생성자
    2. Class Method(메서드)                             ->  클래스의 프로토타입, 메서드 이름, 메서드 디스크립터 
    3. Class Property(속성)                               ->  클래스의 프로토타입, 속성이름
    4. Class Method Parameter(파라미터)        ->  클래스의 프로토타입, 메서드 이름 , 파라미터 인덱스
    5. Class Accessor(접근자)                           ->  클래스의 프로토타입, 메서드 이름, 메서드 디스크립터 

 

 

다음은 데코레이터의 각 인자들을 출력하는 타입스크립트 예제이다.

function classDec (constructor: Function) {
    console.log("Class Decorator 1 arguments");
    console.log(constructor);
    console.log("Class finishied");
}



function propertyDec (classPrototype: Object, propertyName: string) {
    console.log("Property Decorator 2 arguments");
    console.log(classPrototype);
    console.log(propertyName);
    console.log("Property argument finishied");
}


function methodDec(classPrototype: Object, methodName: string, methodDescriptor:  PropertyDescriptor) {
    console.log("method Decorator. 3 arguments");
     console.log(classPrototype);
     console.log(methodName);
     console.log(methodDescriptor);
     console.log("method argument finishied");
     
 }

function parameterDec (constructor: Object, methodName: string, paramindex: number) {
    console.log("parameter Decorator 3 arguments")
    console.log(constructor);
    console.log(methodName);
    console.log(paramindex);
    console.log("parameter finishied");
}

function accessorDec (classPrototype: Object, propertyName: string) {
    console.log("accessor Decorator 2 arguments")
    console.log(classPrototype);
    console.log(propertyName);
    console.log("accessor finishied");
}

@classDec 
class ExClass {

    @propertyDec 
    exProperty = "property";

    @methodDec 
    exMethod (@parameterDec word: string) {
        console.log("Method " + word)
    };

    @accessorDec
    get exAccessor() {
        return "value";
    };

}

각각의 인자들

데코레이터의 실행순서

데코레이터도 일반코드처럼 위에서 아래 순서로 읽히며 실행된다.

 

하지만 하나의 인자에 여러 데코레이터가 적용된 경우 반대로 아래부터  실행된다.

또,  인자의 외부 데코레이터보다 내부 데코레이터 가 먼저 실행된다.(ex 클래스 데코레이터와 클래스 멤버 데코레이터)

//가상의 데코레이터 코드

//주석으로 달아놓은 숫자가 실행순서이다.


//클래스와 클래스 멤버, 메서드와 메서드 파라미터처럼
//서로 상하 포함관계가 존재하는경우엔 내부의 데코레이터가 먼저 실행된다.
//그래서 클래스 데코레이터가 마지막으로 실행된다.

@classDec //6
class ExClass {
    
    //일반적인 코드진행처럼 데코레이터도 위에서부터 아래로 실행된다.
    
    
    @propertyDec2 // 2
    @propertyDec //1
    exProperty = "property";
    
    //하나의 대상에 여러개의 데코레이터가 장식되었을경우
    //아래의 데코레이터가 먼저 실행된다.
    //propertyDec2(propertyDec(exProperty)에서 안쪽이 먼저 실행되는것과
    //같은경우이다.
    

    @methodDec //4
    exMethod (@parameterDec word: string) { //3
        console.log("Method " + word)
    };
    
    //내부의 데코레이터: 파라미터 데코레이터가 먼저 실행된다.

    @accessorDec //5
    get exAccessor() {
        return "value";
    };

}

 

 

 

인자별 데코레이터 예제

1. Class Decorator

 

클래스 데코레이터는 클래스의 constructor(생성자)를 argument로 받아들이는 함수이다.

 

예제를 통해 사용예시를 살펴보자.

다음은 exMethod라는 클래스메서드를 하나 가지고 있는 간단한 클래스이다.

class ExClass {

    exMethod (word: string) {
        console.log("Method " + word)
    };
    // "Method 입력받은word" 를 출력하는 메서드

}

const newClass = new ExClass();

 

메서드 호출

 

 

클래스 데코레이터와 프로토타입을 이용하여 메서드를 번경해 보자.

 

프로토타입 게시글:

 

prototype(프로토타입)과 객체지향 프로그래밍: Typescript Decorator를 위한 Javascript(자바스크립트) 사전

Prototype?(프로토타입)의 의미 사전적 의미: proto: 원래의, 원시적인 + type: 형태, 유형, 카테고리 prototype: 원래의 형태, 원형, 임시 모델 Prototype in javascript(자바스크립트, 이하 JS): 객체의 특징, 행동

batcave.tistory.com

 

 

function classDec (constructor: Function) {
    console.log("Class Decorator 1 arguments");
    console.log(constructor);
    console.log("Class argument finishied");
    
    //데코레이터가 받은 argument: 생성자를 콘솔에 출력해준다.
    // 현재 ExClass는 생성자를 정의하지 않았지만 
    // 기본값으로 생성되는 생성자가 데코레이터의 인자가 된다.

    constructor.prototype.exMethod = function (word: string) {
        console.log("Method was changed " + word)
    }
    // constructor.prototype을 통해 클래스의 프로토타입에 접근할 수 있다.
    // exMethod의 출력내용을 "Method + word"에서
    // "Method was changed + word" 로 바꿔주었다
}


//클래스 데코레이터는 클래스 정의의 윗부분에 작성해준다


@classDec
class ExClass {

    exMethod (word: string) {
        console.log("Method " + word)
    };

}

 

 

이제 콘솔에서 다시 exMethod를 호출하면 달라진 실행결과를 볼 수 있다.

출력결과

출력내용이 "Method hi"에서 "Method was changed hi"로 바뀌었다.

 

 

 

하지만 클래스내용 자체는 변하지 않았다.

데코레이터는 클래스내용을 번경하거나 서브클래스를 정의하지 않고

클래스의 동작을 바꿔주는 기능이기 때문이다.

 

콘솔에 ExClass를 입력하여 클래스 내용을 확인해 보자.

exMethod의 내용은 그대로 "Method" + word이다.

 

2. Method decorator

 

메서드 데코레이터는 클래스의 프로토타입, 메서드의 이름, 메서드의 descriptor(디스크립터)를 

인자로 받아들인다.

 

위에 정의한 클래스를 조금 수정하여 예제를 만들어보자.

// HTML의 Body에 다음과 같은 버튼을 추가해준다.
<button>click me!</button>



//타입스크립트 코드

class ExClass {

    message = "hello";
    exMethod () {
        console.log(this.message)
    };
    //exMethod를 사용할시
    //콘솔에 message 속성을 출력한다

  
}

const button = document.querySelector('button')!;
button.addEventListener('click', newClass.exMethod);
//버튼을 누를시 exMethod를 호출한다.

위의 코드는 동작하지 않는다.

왜냐하면 newClass.exMethod를 호출할 때 이벤트리스너가 this키워드로 message에 접근할 수 있도록

바인딩되어있지 않기 때문이다.

 

이는. bind메서드나 화살표함수를 사용하면 정상적으로 작동하는 코드를 만들 수 있다.

button.addEventListener('click', () => newClass.exMethod());
button.addEventListener('click', newClass.exMethod.bind(newClass));

//위의 두줄중 하나로 코드를 교체시 해결할 수 있다.

 

하지만 메서드 데코레이터로도 위의 문제를 해결할 수 있다.

function methodDec(classPrototype: Object, methodName: string, methodDescriptor:  PropertyDescriptor) {
    console.log("method Decorator. 3 arguments");
     console.log(classPrototype);
     console.log(methodName);
     console.log(methodDescriptor);
     console.log("method argument finishied");
     //메서드 데코레이터들의 인자들을 출력하는 코드
     
     const originalMethod = methodDescriptor.value;
     const adjDescriptor: PropertyDescriptor = {
         get() {
             const boundFn = originalMethod.bind(this);        
             return boundFn;
             
              //descriptor 내부에서 exMethod를 bind 해준 후 반환한다.
         }
     };
     
     return adjDescriptor;
 }
 
 
 
 
 class ExClass {

    message = "hello";

    @methodDec        //메서드 데코레이터를 적용해준다
    exMethod () {
        console.log(this.message)
    };

}

 

버튼을 5번 누른 콘솔창.

정상적으로 바인딩된 모습

 

 

3. Property Decorator

속성데코레이터는 클래스의 프로토타입, 속성이름을 인자로 받아들인다.

속성의 descriptor를 번경하여 대상의 특징을 바꾸어보자.

 

 

class MyClass {
   
    public myProperty = "hello";
  }
  
  const myInstance = new MyClass();

위의 코드에서 myProperty는 public으로 선언되었다.

 

즉, myInstance.myProperty로 값을 호출할 수 있다.

myInstance.myProperty = "value want to change"로 값을 바꿀 수도 있다.

 

 

이제 속성 데코레이터를 통해 "myProperty"를 읽기만 가능하고 수정은 불가능한 요소로 바꾸어보겠다.

function ReadOnly(target: object, propertyKey: string) {
    let value: any;
  
    const getter = () => {
        return value;
    };
    
    const setter = () => {
        console.log("cannot set");
    }
  
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: false
    });
  }
  //대상의 descriptor의내용을 위와같이 수정해주었다 
  // 값을 설정하는 set을 무력화시킴으로써 값의 번경이 불가능하게되었다.
  
  //descriptor는 대상의 속성 및 get과 set을 포함하는 오브젝트이다.
  
  class MyClass {
    @ReadOnly
    public myProperty = "hello";
  }
  
  const myInstance = new MyClass();

 

이렇게 되면 myProperty는 아예 값을 가질 수 없게 된다.

왜냐하면 descriptor의 set이 무력화되어 초기화조차 불가능하기 때문이다. 

 

클래스 내부는 물론이고 밖에서도 설정불가

 

 

 

 

 

 

 

파라미터 데코레이터와 접근자 데코레이터는 좀 더 공부를 하고 다시 작성하겠습니다.

 

읽어주셔서 감사합니다.

오탈자나 잘못된 내용이 있으면 지적해 주세요.

이미지 출처: Next icons created by Roundicons - Flaticon Christmas tree icons created by Pixel perfect - Flaticon Christmas icons created by Freepik - Flaticon