본문 바로가기
언어/타입스크립트

데코레이터 사용법

by SeungYn 2024. 4. 25.

데코레이터는 타입스크립트 5.0에서 추가됐습니다. 데코레이터는 클래스의 기능을 증강하는 함수로 여러 함수에서 공통으로 수행되는 부분을 데코레이터로 만들어두면 좋습니다.

아래 코드는 클래스 메서드들에는 중복되는 start, end 로그가 있습니다.

 

class ANoDeco {
  eat() {
    console.log('start');
    console.log('eat');
    console.log('end');
  }

  work() {
    console.log('start');
    console.log('work');
    console.log('end');
  }

  sleep() {
    console.log('start');
    console.log('sleep');
    console.log('end');
  }
}

 

하지만 데코레이터를 작성한다면 아래 코드처럼 중복된 로직을 제거해 줄 수 있습니다.

데코레이터 함수는 오리지널메서드와, 컨텍스트를 받는데 오리지널메서드는 해당 데코레이터를 작성하는 메서드이며, 컨텍스트는 데코레이터의 정보를 가지고 있습니다.

function startAndEnd<This, Args extends any[], Return>(
  originalMethod: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<
    This,
    (this: This, ...args: Args) => Return
  >
) {
  function replacementMethod(this: This, ...args: Args) {
    console.log(context.name, 'start');
    const result = originalMethod.call(this, ...args);
    console.log(context.name, 'end');
    return result;
  }
  return replacementMethod;
}

class ADeco {
  @startAndEnd
  eat() {
    console.log('eat');
  }
  @startAndEnd
  work() {
    console.log('work');
  }
  @startAndEnd
  sleep() {
    console.log('sleep');
  }
}

const a = new ADeco();
a.eat();

 

결과

eat가 나오는 이유는 context.name에는 데코레이터를 장식하는 메서드의 이름이 있기 때문입니다.

eat start
eat
eat end

 

context의 종류에는 아래와 같이 다양하며

ClassDecoratorContext: 클래스 자체를 장식할 때

ClassMethodDecoratorContext: 클래스 메서드를 장식할 때

ClassGetterDecoratorContext: 클래스 getter를 장식할 때

ClassSetterDecoratorContext: 클래스 setter를 장식할 때

ClassMemberDecoratorContext: 클래스 멤버를 장식할 때

ClassAccessorDecoratorContext: 클래스 accessor를 장식할 때

ClassFieldDecoratorContext: 클래스 필드를 장식할 때

 

context 타입은 아래처럼 되어있습니다.

type Context = {
	kind: string; // 데코레이터 유형 ClassDecoratorContext라면 class
	name: string | symbol; // 장식 대상 이름
	access: { // has, get, set 접근자
		get?(): unknown; 
		set?(value: boolean): void;
		has?(value: unknown): boolean;
	};
	private?: boolean; // private 여부
	static?: boolean; // static 여부
	addInitializer?(initializer: () => void): void; // 초기화 함수
}

 

데코레이터도 매개변수를 넣어줄 수 있습니다.

아래 코드처럼 고차 함수의 형태로 감싸주면 됩니다. 기존 데코레이터에서 원하는 파람을 넣을 수 있도록 래핑 해주면 됩니다.

function startAndEndPram(start = 'start', end = 'end') {
  return function RealDecorator<This, Args extends any[], Return>(
    originalMethod: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<
      This,
      (this: This, ...args: Args) => Return
    >
  ) {
    function replacementMethod(this: This, ...args: Args) {
      console.log(context.name, start);
      const result = originalMethod.call(this, ...args);
      console.log(context.name, end);
      return result;
    }
    return replacementMethod;
  };
}

 

그럼 해당 데코레이터를 적용시킨 후 실행시키면

class AParamDeco {
  @startAndEndPram()
  eat() {
    console.log('eat');
  }
  @startAndEndPram()
  work() {
    console.log('work');
  }
  @startAndEndPram('안녕', '하세요')
  sleep() {
    console.log('sleep');
  }
}

const a = new AParamDeco();
a.sleep();

 

아래처럼 결과가 나옵니다.

sleep 안녕
sleep
sleep 하세요

 

마지막으로 클래스 데코레이털르 알아봅시다.

아래 코드처럼 첫 번째 매개변수는 클래스 타입이고 반환값은 장식 대상 크래스를 상속한 클래스입니다.

해당 데코레이터는 인스턴스 생성시 로깅을 찍어줍니다.

function log<Input extends new (...args: any[]) => any>(
  value: Input,
  context: ClassDecoratorContext
) {
  if (context.kind === 'class') {
    return class extends value {
      constructor(...args: any[]) {
        super(args);
        this.log('클래스 인스턴스 생성!');
      }

      log(msg: string): void {
        console.log(msg);
      }
    };
  }
  return value;
}

@log
class C {}

const c = new C();