Skip to content

타입스크립트 Decorator 이해하기

Vardy edited this page Dec 10, 2023 · 1 revision

타입스크립트 데코레이터 이해하기

서문

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

** 참고 문헌**

Clone this wiki locally