deep-dive2025-02-18·10 min·191/348

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.3

Problem

기존 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 recommended

Analysis

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

  1. 표준 호환: TypeScript 5.0 Decorator는 TC39 표준과 호환되어 JavaScript 생태계와의 호환성이 향상됩니다
  2. context 객체: Decorator는 두 번째 인자로 유용한 context 객체를 받아 클래스/메서드 정보에 접근할 수 있습니다
  3. Initializer 활용: addInitializer를 사용하여 클래스 초기화 시 실행할 로직을 등록할 수 있습니다
  4. 메타데이터 패턴: Decorator를 활용한 메타데이터 패턴은 런타임에 클래스 정보를 동적으로 검색하는 데 유용합니다
  5. 기존 decorator 마이그레이션: 기존 experimental decorator는 tsconfig.json에서 experimentalDecorators: false로 변경하여 마이그레이션할 수 있습니다

이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.