TypeScript 5.0 Decorator 활용: 메타 프로그래밍 기법
TypeScript 5.0에서 도입된 표준 Decorator를 활용한 메타 프로그래밍 기법과 실용적인 사용 사례를 설명합니다.
TypeScript 5.0 Decorator 활용
Introduction
TypeScript 5.0은 TC39의 Stage 3 Decorator 제안을 공식적으로 지원하게 되었습니다. 기존 TypeScript decorator는 experimental decorator였지만,新版는 표준 JavaScript decorator와 호환됩니다. Decorator는 클래스, 메서드, 프로퍼티 등에 메타데이터를 추가하고 수정하는 강력한 메타 프로그래밍 기법입니다. 이 글에서는 TypeScript 5.0 Decorator의 활용 방법과 실제 사례를 다루겠습니다.
Environment
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"strict": true
}
}# TypeScript 버전 확인
npx tsc --version
# 5.3.3Problem
기존 experimental Decorator의 한계:
// 기존 decorator (deprecated 방식)
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
return original.apply(this, args);
};
}
class UserService {
@Log
getUser(id: number) {
return { id, name: 'Joel' };
}
}
// experimentalDecorators: true 필요
// TypeScript 전용 문법으로 JavaScript 표준과 불일치# experimental Decorator 사용 시 경고
npx tsc --experimentalDecorators
# warning TS1219: Experimental decorators are not recommendedAnalysis
TypeScript 5.0 Decorator의 내부 동작을 분석했습니다:
// 새로운 Decorator 시그니처
type ClassDecorator = (value: Function, context: {
kind: 'class';
name: string | undefined;
addInitializer(initializer: () => void): void;
}) => Function | void;
type MethodDecorator = (
value: Function,
context: {
kind: 'method';
name: string | symbol;
static: boolean;
private: boolean;
access: { has(obj: object): boolean; get(obj: object): Function };
addInitializer(initializer: () => void): void;
}
) => Function | void;
// decorator context 정보 확인
function Inspect(target: any, context: any) {
console.log('Kind:', context.kind);
console.log('Name:', context.name);
console.log('Static:', context.static);
console.log('Private:', context.private);
}// decorator 체이닝
function First() {
return function (target: any, context: any) {
console.log('First decorator');
};
}
function Second() {
return function (target: any, context: any) {
console.log('Second decorator');
};
}
@First()
@Second()
class MyClass {}
// 실행 순서: Second, First (위에서 아래로)Solution
1단계: 기본 Decorator 구현
// 메서드 로깅 decorator
function Log(
value: Function,
context: {
kind: 'method';
name: string | symbol;
static: boolean;
}
) {
if (context.kind === 'method') {
return function (this: any, ...args: any[]) {
console.log(`[${new Date().toISOString()}] Calling ${String(context.name)}`);
console.log(`Arguments:`, args);
const startTime = performance.now();
const result = value.apply(this, args);
const duration = performance.now() - startTime;
console.log(`Result:`, result);
console.log(`Duration: ${duration.toFixed(2)}ms`);
return result;
};
}
}
// 클래스 프로퍼티 검증 decorator
function Validate(
value: any,
context: {
kind: 'field';
name: string | symbol;
static: boolean;
addInitializer(initializer: () => void): void;
}
) {
if (context.kind === 'field') {
return function (initialValue: any) {
if (initialValue === undefined || initialValue === null) {
throw new Error(`Field '${String(context.name)}' cannot be null or undefined`);
}
return initialValue;
};
}
}
// 사용 예
class UserService {
@Log
getUser(id: number) {
return { id, name: 'Joel' };
}
@Validate
userName: string = 'Joel';
}2단계: 실용적인 Decorator 패턴
// 자동 시간 측정 decorator
function Measure(
value: Function,
context: {
kind: 'method';
name: string | symbol;
}
) {
if (context.kind === 'method') {
return function (this: any, ...args: any[]) {
const label = `${String(context.name)}`;
console.time(label);
const result = value.apply(this, args);
// Promise 처리
if (result instanceof Promise) {
return result.finally(() => console.timeEnd(label));
}
console.timeEnd(label);
return result;
};
}
}
// 재시도 decorator
function Retry(maxRetries: number = 3, delay: number = 1000) {
return function (
value: Function,
context: {
kind: 'method';
name: string | symbol;
}
) {
if (context.kind === 'method') {
return async function (this: any, ...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await value.apply(this, args);
} catch (error) {
lastError = error as Error;
console.warn(`Attempt ${attempt} failed: ${lastError.message}`);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
}
throw lastError!;
};
}
};
}
// 사용 예
class APIClient {
@Measure
@Retry(3, 1000)
async fetchData(url: string) {
const response = await fetch(url);
return response.json();
}
}3단계: 메타데이터 활용
// 커스텀 메타데이터 저장
const metadata = new Map>();
function Metadata(key: string, value: any) {
return function (
target: any,
context: {
kind: 'method' | 'field' | 'class';
name: string | symbol;
}
) {
const className = target.constructor?.name || target.name;
if (!metadata.has(className)) {
metadata.set(className, new Map());
}
const classMetadata = metadata.get(className)!;
classMetadata.set(`${String(context.name)}:${key}`, value);
};
}
// 권한 검증 decorator
function RequirePermission(permission: string) {
return function (
value: Function,
context: {
kind: 'method';
name: string | symbol;
}
) {
if (context.kind === 'method') {
return function (this: any, ...args: any[]) {
const user = this.getCurrentUser();
if (!user.permissions.includes(permission)) {
throw new Error(`Permission denied: ${permission}`);
}
return value.apply(this, args);
};
}
};
}
// 사용 예
class AdminController {
@Metadata('route', '/admin/users')
@Metadata('method', 'GET')
@RequirePermission('admin:read')
getUsers() {
return [{ id: 1, name: 'Admin' }];
}
@Metadata('route', '/admin/users')
@Metadata('method', 'DELETE')
@RequirePermission('admin:delete')
deleteUser(id: number) {
return { success: true };
}
}
// 메타데이터 조회
function getRouteMetadata(className: string) {
return metadata.get(className);
} Lessons Learned
- 표준 호환: TypeScript 5.0 Decorator는 TC39 표준과 호환되어 JavaScript 생태계와의 호환성이 향상됩니다
- context 객체: Decorator는 두 번째 인자로 유용한 context 객체를 받아 클래스/메서드 정보에 접근할 수 있습니다
- Initializer 활용:
addInitializer를 사용하여 클래스 초기화 시 실행할 로직을 등록할 수 있습니다 - 메타데이터 패턴: Decorator를 활용한 메타데이터 패턴은 런타임에 클래스 정보를 동적으로 검색하는 데 유용합니다
- 기존 decorator 마이그레이션: 기존 experimental decorator는
tsconfig.json에서experimentalDecorators: false로 변경하여 마이그레이션할 수 있습니다
이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.