icon

커스텀 데코레이터 만들기


 NestJS에서 데코레이터는 클래스, 메서드, 접근자, 프로퍼티, 매개변수에 주석을 달거나 수정하는 데 사용되는 특별한 종류의 선언입니다.

 데코레이터를 통해 코드를 더 선언적으로 만들고 반복을 줄일 수 있습니다.

데코레이터의 기본 개념

 TypeScript 데코레이터는 @expression 형태로 사용되며 expression은 데코레이팅된 선언에 대한 정보와 함께 런타임에 호출되는 함수여야 합니다.

데코레이터 유형

 1. 매개변수 데코레이터

  • 매개변수 데코레이터는 메서드의 매개변수에 적용됩니다.
function Log(target: any, key: string, index: number) {
  console.log(`Parameter at index ${index} in ${key} is decorated`);
}
 
class Example {
  greet(@Log name: string) {
    return `Hello, ${name}!`;
  }
}

 2. 메서드 데코레이터

  • 메서드 데코레이터는 메서드 선언 직전에 적용됩니다.
function Measure() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const start = performance.now();
      const result = originalMethod.apply(this, args);
      const finish = performance.now();
      console.log(`${propertyKey} executed in ${finish - start}ms`);
      return result;
    };
    return descriptor;
  };
}
 
class Example {
  @Measure()
  slowMethod() {
    // Some time-consuming operation
  }
}

 3. 클래스 데코레이터

  • 클래스 데코레이터는 클래스 선언 직전에 적용됩니다.
function Controller(prefix: string) {
  return function (constructor: Function) {
    Reflect.defineMetadata('prefix', prefix, constructor);
  };
}
 
@Controller('/api')
class SomeController {}

 4. 프로퍼티 데코레이터

  • 프로퍼티 데코레이터는 프로퍼티 선언 직전에 적용됩니다.
function IsString() {
  return function (target: any, propertyKey: string) {
    let value: string;
    const getter = function () {
      return value;
    };
    const setter = function (newVal: string) {
      if (typeof newVal !== 'string') {
        throw new Error('Value must be a string');
      }
      value = newVal;
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
    });
  };
}
 
class User {
  @IsString()
  name: string;
}

커스텀 매개변수 데코레이터 만들기

 NestJS에서 커스텀 매개변수 데코레이터를 만들어 요청 데이터를 추출하거나 변환할 수 있습니다.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
 
export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
 
    return data ? user?.[data] : user;
  },
);
 
// 사용
@Get('profile')
getProfile(@User('email') email: string) {
  return `Profile of user with email: ${email}`;
}

메서드 데코레이터를 통한 기능 추가

 메서드 데코레이터를 사용하여 로깅, 캐싱, 권한 검사 등의 기능을 추가할 수 있습니다.

import { SetMetadata } from '@nestjs/common';
 
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
 
// 사용
@Post()
@Roles('admin')
create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

클래스 데코레이터를 통한 클래스 수정

 클래스 데코레이터를 사용하여 클래스의 정의나 동작을 변경할 수 있습니다.

function AddMethod(target: Function) {
  target.prototype.newMethod = function() {
    console.log('This is a new method');
  }
}
 
@AddMethod
class MyClass {}
 
const obj = new MyClass();
(obj as any).newMethod(); // Outputs: "This is a new method"

데코레이터 조합과 실행 순서

 여러 데코레이터를 조합하여 사용할 때, 실행 순서는 다음과 같습니다.

  1. 각 데코레이터의 표현식은 위에서 아래로 평가됩니다.
  2. 그 결과는 아래에서 위로 함수로 호출됩니다.
function first() {
  console.log("first(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("first(): called");
  };
}
 
function second() {
  console.log("second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("second(): called");
  };
}
 
class ExampleClass {
  @first()
  @second()
  method() {}
}

 출력

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called

데코레이터 팩토리

 데코레이터 팩토리를 사용하면 데코레이터를 구성할 수 있습니다.

function Logger(prefix: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log(`${prefix}: Entering ${propertyKey}`);
      const result = originalMethod.apply(this, args);
      console.log(`${prefix}: Exiting ${propertyKey}`);
      return result;
    };
    return descriptor;
  };
}
 
class Example {
  @Logger('MyClass')
  doSomething() {
    // Method implementation
  }
}

리플렉션, 메타데이터 기반 데코레이터

 reflect-metadata 라이브러리를 사용하여 더 복잡한 데코레이터를 만들 수 있습니다.

import 'reflect-metadata';
 
const ROUTE_METADATA = Symbol('route');
 
function Route(path: string) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata(ROUTE_METADATA, path, target, propertyKey);
  };
}
 
function Controller(prefix: string) {
  return function (constructor: Function) {
    for (let propertyKey of Object.getOwnPropertyNames(constructor.prototype)) {
      const path = Reflect.getMetadata(ROUTE_METADATA, constructor.prototype, propertyKey);
      if (path) {
        console.log(`Mapped route: ${prefix}${path} to ${propertyKey}`);
      }
    }
  };
}
 
@Controller('/api')
class SomeController {
  @Route('/users')
  getUsers() {}
}

Best Practices와 주의사항

  1. 단일 책임 원칙 준수 : 각 데코레이터는 하나의 명확한 목적을 가져야 합니다.
  2. 재사용성 고려 : 데코레이터를 설계할 때 재사용 가능성을 고려하세요.
  3. 타입 안전성 유지 : TypeScript의 타입 시스템을 최대한 활용하여 타입 안전성을 보장하세요.
  4. 성능 고려 : 데코레이터가 성능에 미치는 영향을 고려하세요. 특히 자주 호출되는 메서드에 적용할 때 주의가 필요합니다.
  5. 테스트 작성 : 데코레이터의 동작을 검증하는 단위 테스트를 작성하세요.
  6. 문서화 : 복잡한 데코레이터의 경우, 그 목적과 사용법을 명확히 문서화하세요.
  7. 조합 고려 : 여러 데코레이터를 조합하여 사용할 때 실행 순서를 고려하세요.
  8. 부작용 주의 : 데코레이터가 예기치 않은 부작용을 일으키지 않도록 주의하세요.
  9. 메타데이터 활용 : 필요한 경우 메타데이터를 활용하여 더 강력한 데코레이터를 만들 수 있습니다.
  10. 프레임워크 제공 기능 활용 : NestJS가 제공하는 데코레이터 관련 기능(예 : createParamDecorator)을 적극 활용하세요.