커스텀 데코레이터 만들기
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"
데코레이터 조합과 실행 순서
여러 데코레이터를 조합하여 사용할 때, 실행 순서는 다음과 같습니다.
- 각 데코레이터의 표현식은 위에서 아래로 평가됩니다.
- 그 결과는 아래에서 위로 함수로 호출됩니다.
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와 주의사항
- 단일 책임 원칙 준수 : 각 데코레이터는 하나의 명확한 목적을 가져야 합니다.
- 재사용성 고려 : 데코레이터를 설계할 때 재사용 가능성을 고려하세요.
- 타입 안전성 유지 : TypeScript의 타입 시스템을 최대한 활용하여 타입 안전성을 보장하세요.
- 성능 고려 : 데코레이터가 성능에 미치는 영향을 고려하세요. 특히 자주 호출되는 메서드에 적용할 때 주의가 필요합니다.
- 테스트 작성 : 데코레이터의 동작을 검증하는 단위 테스트를 작성하세요.
- 문서화 : 복잡한 데코레이터의 경우, 그 목적과 사용법을 명확히 문서화하세요.
- 조합 고려 : 여러 데코레이터를 조합하여 사용할 때 실행 순서를 고려하세요.
- 부작용 주의 : 데코레이터가 예기치 않은 부작용을 일으키지 않도록 주의하세요.
- 메타데이터 활용 : 필요한 경우 메타데이터를 활용하여 더 강력한 데코레이터를 만들 수 있습니다.
- 프레임워크 제공 기능 활용 : NestJS가 제공하는 데코레이터 관련 기능(예 :
createParamDecorator
)을 적극 활용하세요.