데코레이터
데코레이터는 클래스, 메서드, 프로퍼티 등 메타데이터를 추가하거나 동작을 수정할 수 있게 해주는 함수임
타입스크립트에서 @ 기호와 함께 사용하며 이는 대상의
정의 시점에 실행됨대상의 동작을 확장하거나 메타데이터를 부착할 수 있음
메타프로그래밍과 연관 관계
메타프로그래밍은 데이터를 다루는게 아닌 코드 자체를 다루는 방법임
데코레이터는 대상을 인자로 받아서 조작하므로 코드를 데이터처럼 다루는 셈임
그래서 데코레이터는 메타프로그래밍의 대표적인 구현 방식 중 하나임
타입스크립트 데코레이터 사용을 위한 설정
데코레이터의 경우 현재 TC39 Stage 3인 상태로
typescript@5이상의 버전에서는 기본으로 제공하고 있음하지만 이전까지 사용했던 레거시 형식의 문법이 여전히 많이 사용되고있는 상황임
기술적으로는 가능하지만 주요 라이브러리/프레임워크는 여전히 에코시스템이 따라오지 못하고 있음
구버전 데코레이터 문법을 사용하기 위해서는
experimentalDecorators옵션 활성화가 필요함
{
"compilerOptions": {
// ...
"experimentalDecorators": true
},
}데코레이터 종류
클래스 데코레이터
클래스 선언 직전에 붙이는 데코레이터로 생성자 함수를 인자로 받게됨
클래스의 정의를 관찰, 수정 등이 가능하며 완전히 새로운 클래스로 교체도 가능함
데코레이터가 값을 반환하면 원래 클래스의 생성자를 반환된 값으로 대체하게됨
function AddTimestamp<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
createdAt = new Date();
};
}
@AddTimestamp
class Seat {
constructor(private seatNumber: string, private isAvailable: boolean) {}
}
const seat = new Seat("123", true);
// Seat {
// seatNumber: '123',
// isAvailable: true,
// createdAt: 2025-12-10T11:05:00.357Z
// }
console.log(seat);메서드 데코레이터
메서드 데코레이터는 메서드 선언 직전에 붙이는 데코레이터로 런타임에서 3개의 인자와 함께 호출됨
메서드의
PropertyDescriptor에 접근이 가능해서 메서드를 수정하거나 다른 함수로 대체가 가능함로깅, 성능 측정, 권한 체크 등 메서드 실행 전/후에 공통적인 로직을 삽입할 때 유용함
메서드 데코레이터의 인자
target :인스턴스의 경우 클래스의 프로토타입이며 아닌 경우 생성자 함수를 인자로 받음
propertyKey: 메서드의 이름
propertyDescriptor : 메서드의 프로퍼티 객체
value, writeable, enumerable 등 속성을 가지는데 value 내부에 실제 함수가 담겨있음
해당 value를 다른 함수로 교체하면 원본 메서드를 감싸는 래퍼 함수를 만들어서 사용이 가능함
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}예제
위에서 설명한 인자처럼 descriptor.value에는 원본 메서드가 들어가있는데 해당 값을 변수에 미리 저장해놓음
메서드가 호출되면
console.log가 호출되고originalMethod.apply를 통해서 원본 메서드를 호출하게됨이 때 여기서
function을 사용하는 경우 오류가 발생하는데 이는this가 결정되는 타이밍이 달라서임arrow function: 정의될 때의 this를 캡쳐 -> 데코레이터가 실행되는 시점의 this임function: 호출될 때의 this를 동적으로 받음 -> 메서드가 속해있는 클래스 인스턴스를 가르킴
function LogExecution(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 원본 메서드를 저장
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[${new Date().toISOString()}] ${propertyKey} 호출, 인자:`, args);
const result = originalMethod.apply(this, args);
console.log(`[${new Date().toISOString()}] ${propertyKey} 완료, 결과:`, result);
return result;
};
}
class PaymentService {
@LogExecution
processPayment(userId: number, amount: number) {
return { success: true, userId, amount };
}
}
const service = new PaymentService();
const paymentResult = service.processPayment(1, 5000);
console.log(paymentResult);
// [2025-01-01T12:00:00.000Z] processPayment 호출, 인자: [1, 5000]
// [2025-01-01T12:00:00.000Z] processPayment 완료, 결과: { success: true, userId: 1, amount: 5000 }
// { success: true, userId: 1, amount: 5000 }접근자 데코레이터
클래스의 getter, setter 선언 직전에 붙이는 데코레이터로 메서드 데코레이터와 동일한 시그니처를 가짐
접근자의 propertyDescriptor에 접근이 가능해서 메서드처럼 제어가 가능함
주로 값 검증, 값 변환, 접근 제어 등의 용도로 사용하게됨
function ValidatePositive(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSetter = descriptor.set;
descriptor.set = function (value: number) {
if (value < 0) {
throw new Error(`${propertyKey}: 금액은 0 이상이어야 합니다`);
}
if (originalSetter) {
originalSetter.call(this, value);
}
};
}
class SeatUsage {
private _chargedAmount: number = 0;
get chargedAmount() {
return this._chargedAmount;
}
@ValidatePositive
set chargedAmount(value: number) {
this._chargedAmount = value;
}
}
const usage = new SeatUsage();
console.log(usage); // SeatUsage { _chargedAmount: 0 }
usage.chargedAmount = 3000;
console.log(usage); // SeatUsage { _chargedAmount: 3000 }
usage.chargedAmount = -1000; // Error: chargedAmount: 금액은 0 이상이어야 합니다프로퍼티 데코레이터
프로퍼티를 선언하기 직전에 붙이는 데코레이터로 target, propertKey 총 2개의 인자를 받음
프로토타입에 멤버 변수를 정의하는 시점에는 인스턴스 프로퍼티 설명이 불가능해서 descriptor를 받지 않음
위 같은 제약으로 인해서 주로
이 프로퍼티가 선언되었음같은 사실을 기록하기 위한 용도로 사용하게됨이 경우 메타데이터를 외부에 저장해두고 나중에 활용하는 패턴이 일반적임
const columnMetadata: Map<string, any> = new Map();
type ColumnOptions = {
type: string;
nullable?: boolean;
};
function Column(options: ColumnOptions) {
return function (target: any, propertyKey: string) {
const className = target.constructor.name;
const key = `${className}.${propertyKey}`;
columnMetadata.set(key, options);
};
}
class Member {
@Column({ type: "varchar", nullable: false })
name: string;
@Column({ type: "int", nullable: true })
remainingTime: number;
}
console.log(columnMetadata.get("Member.name"));
// { type: 'varchar', nullable: false }
console.log(columnMetadata.get("Member.remainingTime"));
// { type: 'int', nullable: true }파라미터 데코레이터
메서드의 파라미터 선언 직전에 붙이고 target, propertyKey, parameterIndex 3개의 인자를 받음
단독으로는 파라미터의 값을 수정하거나 검증이 불가능하며 메타데이터를 저장해두고 메서드 데코레이터랑 같이 쓰는게 일반적임
데코레이터를 통해서 이 파라미터는 꼭 전달해야함 등 정보를 기록하고 메서드 데코레이터가 실제 검증을 수행하는 느낌임
const requiredParams: Map<string, number[]> = new Map();
function Required(target: any, propertyKey: string, parameterIndex: number) {
const key = `${target.constructor.name}.${propertyKey}`;
const indices = requiredParams.get(key) || [];
indices.push(parameterIndex);
requiredParams.set(key, indices);
}
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const key = `${target.constructor.name}.${propertyKey}`;
descriptor.value = function (...args: any[]) {
const indices = requiredParams.get(key) || [];
for (const index of indices) {
if (!args[index]) {
throw new Error(`매개변수 ${index}번은 필수입니다`);
}
}
return originalMethod.apply(this, args);
};
}
class SeatService {
@Validate
assignSeat(@Required seatNumber: number, @Required memberId: number) {
return { seatNumber, memberId };
}
}
const seatService = new SeatService();
seatService.assignSeat(1, 100);
// @ts-ignore
seatService.assignSeat(1, undefined); // Error: 매개변수 1번은 필수입니다데코레이터 팩토리
위 5가지 데코레이터 예제에서 일부 데코레이터는 직접 인자를 받는 방식으로 동작했음
이처럼 데코레이터에 인자를 전달하기 위해서는
데코레이터 팩토리를 사용해야함데코레이터 팩토리는 데코레이터를 반환하는 함수로 외부에서 인자를 받아서 데코레이터의 동작 커스텀이 가능해짐
/**
* 일반 데코레이터 방식으로 파라미터를 직접 받을 수 없음
*/
function SimpleLog(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(`${propertyKey} 정의됨`);
}
/**
* 데코레이터 팩토리로 인자를 받을 수 있음
*/
function Route(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (!target.constructor.routes) {
target.constructor.routes = [];
}
target.constructor.routes.push({ path, method: propertyKey });
};
}
class SeatController {
@Route("/seats")
getAllSeats() {
return [];
}
@Route("/seats/:id")
getSeatById(id: number) {
return { id };
}
}
console.log((SeatController as any).routes);
// [{ path: '/seats', method: 'getAllSeats' }, { path: '/seats/:id', method: 'getSeatById' }]데코레이터의 실행 순서
하나의 선언에 여러개의 데코레이터 연동이 가능한데 실행 순서의 경우 두 단계로 나뉘게됨
각 데코레이터는 표현식이
위 -> 아래로 평가되고, 그 다음결과가 아래에서 위로 함수로써 호출하게됨
function First() {
console.log("First 평가");
return function (target: any, key: string, desc: PropertyDescriptor) {
console.log("First 실행");
};
}
function Second() {
console.log("Second 평가");
return function (target: any, key: string, desc: PropertyDescriptor) {
console.log("Second 실행");
};
}
class SeatReservationService {
@First()
@Second()
createOrder() {}
}
// 출력:
// First 평가
// Second 평가
// Second 실행
// First 실행