타입스크립트 Decorator 이해하기

JeongYong·2023년 10월 30일
2
post-thumbnail

Decorator는 타입스크립트의 강력한 기능 중 하나로, 코드의 가독성과 유지보수성을 향상시키며, 중복된 로직을 감소시키는 데 사용하는 도구입니다. Decorator는 객체 지향 프로그래밍과 AOP(Aspect-Oriented Programming)와 밀접한 관련이 있으며, 다양한 프레임워크와 라이브러리에서 활발하게 활용되고 있습니다. 이 글에서는 Decorator의 기본 개념과 주요 목적, Decorator 팩토리, Decorator 합성, Decorator 타입, 사용 법을 이야기해 보겠습니다.

Decorator 기본 개념

Decorator 패턴이란

Decorator 패턴이란 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴입니다. 즉 기본 기능에 추가할 기능이 많은 경우 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 경우 Decorator 객체로 조합해서 사용하는 설계 방식입니다.

타입스크립트 Decorator 란

타입스크립트 Decorator는 클래스, 메서드, 접근자, 프로퍼티, 매개변수 등의 맴버에 메타데이터를 추가하고, 동작을 변경하거나 확장하는데 사용되는 기능입니다.
Decorator는 @expression 형식을 사용합니다. 여기서 expression은 데코레이팅 된 선언에 대한 정보와 함께 런타임에 호출되는 함수여야 합니다. 즉 @expression은 클래스, 메서드, 접근자, 프로퍼티, 매개변수 등의 선언 앞에 위치시킴으로써 사용됩니다.

function expression(target) {
    // 'target'를 이용해서 수행
}

@expression
class Example {}

이렇게 Decorator 함수는 대상이 되는 클래스, 메서드, 접근자, 프로퍼티, 매개변수 등의 메타데이터를 받아 작업을 수행합니다.

Decorator의 주요 목적 (사용 이유)

  1. 관심사 분리 (Separation of Concerns): 데코레이터를 사용하면 코드의 관심사를 분리하여 모듈화할 수 있습니다. 특정 기능, 로직 또는 관심 분야를 독립적인 데코레이터로 정의하고, 이를 적용할 대상(클래스, 메서드, 프로퍼티)에 연결할 수 있습니다. 이로써 코드의 가독성과 유지보수성이 향상됩니다.

  2. 재사용성: 데코레이터를 사용하면 동일한 데코레이터를 여러 다른 클래스 또는 메서드에 적용할 수 있으며, 코드 중복을 줄이고 재사용성을 높일 수 있습니다. 특히, 공통된 기능(예: 로깅, 캐싱, 보안)을 여러 다른 컴포넌트에 쉽게 적용할 수 있습니다.

  3. 연산의 조합: 데코레이터를 조합하여 다양한 동작을 구현할 수 있습니다. 여러 데코레이터를 함께 사용하여 복잡한 동작을 구성하고, 필요에 따라 동작을 추가하거나 제거할 수 있습니다.

  4. 오픈/폐쇄 원칙 준수: 데코레이터는 소프트웨어 개발의 SOLID 원칙 중 하나인 "개방/폐쇄 원칙(Open/Closed Principle)"을 준수하는 방법으로 사용됩니다. 이 원칙에 따르면 소프트웨어 엔티티(클래스, 모듈 등)는 확장에는 열려 있어야 하고, 수정에는 폐쇄적이어야 한다고 정의합니다. 데코레이터를 사용하면 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있습니다.

  5. 메타데이터 추가: 데코레이터는 클래스, 메서드, 프로퍼티 등에 메타데이터를 추가하는 데 사용됩니다. 이 메타데이터는 런타임에 읽을 수 있으며, 예를 들어 Angular 프레임워크에서 컴포넌트, 서비스, 라우터 등을 정의할 때 사용됩니다.

  6. 횡단 관심사(Cross-Cutting Concerns) 처리: AOP의 일부로 사용될 때, 데코레이터는 횡단 관심사(로깅, 보안, 트랜잭션 관리 등)를 처리하는 데 유용합니다. 이러한 관심사는 애플리케이션의 여러 부분에 영향을 미치며, 데코레이터를 사용하면 중복 코드를 방지하고 한 곳에서 관리할 수 있습니다.

  7. 가독성 향상: 데코레이터를 사용하면 코드에 의도가 명확히 드러나고, 코드의 가독성이 향상됩니다. 어떤 동작이 어떤 데코레이터를 통해 적용되는지 명시적으로 나타낼 수 있습니다.

Decorator 팩토리

데코레이터 팩토리 함수는 데코레이터를 감싸는 래퍼 함수입니다.
데코레이터가 선언에 적용되는 방식을 원하는 대로 바꾸고 싶다면 데코레이터 팩토리를 다음과 같이 작성할 수 있습니다.

function factory(param1: any) { //데코레이터 팩토리
   return function(target) { //데코레이터
       // 'target'과 'param1'을 이용해서 수행
   }
}

데코레이터 팩토리는 단순히 데코레이터가 런타임에 호출할 표현식을 반환하는 함수입니다.

Decorator 합성

데코레이터는 하나의 타겟의 여러 데코레이터를 적용할 수 있습니다. 이를 데코레이터 합성이라고 합니다.
다음 예제와 같이 여러 데코레이터를 적용할 수 있습니다.

function first() {
   console.log("first(): factory 평가됨");
   return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log("first(): 호출됨");
   }
}

function second() {
   console.log("second(): factory 평가됨");
   return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      console.log("second(): 호출됨");
   }
}

class Example {
   @first()
   @second()
   method(){}
}

출력 결과

"first(): factory 평가됨"
"second(): factory 평가됨"
"second(): 호출됨"
"first(): 호출됨"

출력 결과를 보면
데코레이터 표현식은 위에서 아래로 평가되고,
데코레이터 함수는 아래에서 위로 호출되는 것을 볼 수 있다.

Decorator 타입

타입스크립트에서 데코레이터의 타입은 데코레이터 함수가 받는 인자의 형태와 반환 값에 관련이 있습니다. 데코레이터는 클래스, 메서드, 접근자, 프로퍼티 또는 매개변수에 적용할 수 있으며, 각각에 대한 데코레이터 타입이 조금씩 다릅니다.

Class Decorator

클래스 데코레이터는 클래스 선언 앞에 위치합니다. 생성자에 적용되며 클래스 정의를 관찰, 수정 또는 교체하는 데 사용할 수 있습니다.

클래스 데코레이터 표현식은 데코레이팅된 클래스의 생성자를 유일한 인수로 런타임에 함수로 호출됩니다.

클래스 데코레이터가 값을 반환하면 데코레이팅된 클래스가 수정, 확장됩니다. (반환 값은 새로운 클래스)

아래 코드는 클래스 맴버를 동적으로 확장하는 코드입니다.

function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        member1 = '새로운 값으로 덮어씌움'; 
        member2 = 'new memeber'; // 새로운 맴버 추가
    };
}

@classDecorator
class Example {
    member1: string;

    constructor(m: string) {
        this.member1 = m;
    }
}

let t = new Example('기존 값');
console.log(t);
console.log(t.member1); // 'override'
console.log((t as any).member2); //데코레이터로 확장된 속성은 타입 단언 필요!
class_1 { member1: '새로운 값으로 덮어씌움', member2: 'new memeber' }
새로운 값으로 덮어씌움
new memeber

아래 코드는 Decoraotr 팩토리를 이용한 예제입니다.

function classDecorator(param1: string, param2: string) {
    return function <T extends { new (...args: any[]): {} }>(constructor: T) {
       return class extends constructor {
          member1 = param1;
          member2 = param2;
       };
    };
 }

@classDecorator('새로운 값', 'member2')
class Example {
    member1: string;

    constructor(m: string) {
        this.member1 = m;
    }
}

let t = new Example('기존 값');
console.log(t);
console.log(t.member1); // 'override'
console.log((t as any).member2);
class_1 { member1: '새로운 값', member2: 'member2' }
새로운 값
member2

아래 코드는 프로토타입을 이용하여 확장하는 예제입니다.

function classDecorator<T extends { new (...args: any[]): {} }>(constructorFn: T) {

    // 프로토타입으로 새로운 프로퍼티를 추가
    constructorFn.prototype.test2 = function () {
       console.log('test2');
    };
    constructorFn.prototype.age = '26';
 
    // 클래스를 프로퍼티에 상속시켜 새로운 멤버를 추가 설정
    return class extends constructorFn {
       public name = 'sjy';
 
       constructor(...args: any[]) {
          super(args);
       }
 
       public test1() {
          console.log('test1');
       }
    };
 }

@classDecorator
class Example {}

let e = new Example();

(e as any).test1(); //데코레이터로 동적으로 추가된 형태는 타입 단언 사용
(e as any).test2();

console.log((e as any).age);

console.log(e);
test1
test2
26
class_1 { name: 'sjy' }

Method Decorator

메서드 데코레이터는 메서드 선언 앞에 위치합니다. 데코레이터는 메서드의 Property Descriptor에 적용되며 메서드 정의를 관찰, 수정 또는 대체하는 데 사용할 수 있습니다.

메소드 데코레이터 표현식은 런타임에 다음 세 개의 인수와 함께 함수로 호출됩니다.

첫 번째 인수는 정적 맴버에 대한 클래스의 생성자 함수, 인스턴스 맴버에 대한 클래스의 프로토타입
두 번째 인수는 메서드 이름
세 번째 인수는 메서드의 Property Descriptor

아래 코드는 데코레이팅된 메서드가 호출될 때 동작을 추가하는 예제입니다.

function methodDecorator() {
    return function (target: any, property: string, descriptor: PropertyDescriptor) {
 
       // descriptor.value는 test() 함수를 가리킵니다.
       let originMethod = descriptor.value;
 
       // 기존의 test() 함수의 내용을 다음과 같이 바꿔줍니다.
       descriptor.value = function (...args: any) {
          console.log('동작 추가 1');
          console.log("동작 추가 2");
          originMethod.apply(this, args); 
       };
    };
 }
 
 class Example {

    @methodDecorator()
    test() {
       console.log("기존 동작");
    }
 }
 
 let e = new Example();
 e.test();
동작 추가 1
동작 추가 2
기존 동작

여기서 this로 originMethod를 apply로 연결해 준다. this로 연결해 주지 않으면 현재 실행되는 환경(컨텍스트)을 알 수 없기 때문에 originMethod가 제대로 동작하지 않는다.

Property Decorator

프로퍼티 데코레이터는 포르퍼티 선언 앞에 위치합니다.

프로퍼티 데코레이터의 표현식은 런타임에 다음 두 개의 인수가 함께 함수로 호출됩니다.

첫 번째는 정적 맴버에 대한 클래스의 생성자 함수, 인스턴스 맴버에 대한 클래스의 프로토타입

두 번째는 프로퍼티 이름

아래 코드는 속성의 부가적인 설정을 하는 예제 입니다.

function writable(writable: boolean) {
    return function (target: any, decoratedPropertyName: any): any {
        Object.defineProperty(target, decoratedPropertyName, {
            writable,
        });
    };
}

class Example {
    @writable(false)
    data: number;
    @writable(true)
    data2: number;

    constructor(num: number, num2: number) {
        this.data = num;
        this.data2 = num2;
    }
}

let e = new Example(500, 1000);
e.data *= 2;  //writable이 false이기 때문에 런타임 에러 발생
e.data2 *= 2;

Parameter Decorator

매개변수 데코레이터는 매개 변수 선언 앞에 위치합니다.

매개변수 데코레이터 표현식은 런타임시 다음 세 개의 인수와 함께 함수로 호출됩니다.

첫 번째는 정적 맴버에 대한 클래스의 생성자 함수, 인스턴스 맴버에 대한 클래스의 프로토타입
두 번째는 파리미터 이름
세 번째는 파라미터의 순서 index ex) fn(p1, p2, p3) 일 때 p1은 0, p2는 1, p3은 2임

아래 코드는 파라미터 데코레이터를 사용하는 예시입니다.

function parameterDecorator(target: any, paramName: string, paramIndex: number) {
    console.log(paramName);
 }

class Example {
    test(@parameterDecorator m: string) {
        console.log(m);
    }
}

const e = new Example();
e.test("hi");

Decorator 호출 순서

  1. property
  2. method
  3. parameter
  4. class

참고 문헌

0개의 댓글