Logo

개발자가 절대 알려주지 않는 TypeScript 데코레이터 숨겨진 비밀 - 3년 실무 경험으로 터득한 메타프로그래밍 마스터 노하우

🎯 요약

@Controller, @Injectable 같은 걸 보면서 "저건 대체 뭐지?" 하고 궁금했던 적 있나요? 저도 처음엔 그냥 마법의 주문 같았어요. 하지만 3년간 NestJS와 TypeORM으로 실무 프로젝트를 진행하면서 깨달은 건 데코레이터야말로 TypeScript의 진짜 숨겨진 보석이라는 거죠. 한번 제대로 익히면 반복 코드는 확 줄고 코드는 훨씬 깔끔해집니다.

📋 목차

  1. 데코레이터가 뭔지부터 차근차근
  2. 4가지 데코레이터 타입 완전 해부
  3. 실무에서 바로 쓰는 커스텀 데코레이터
  4. NestJS 스타일 고급 패턴 마스터
  5. 메타데이터와 reflect-metadata 활용법
  6. 실전 프로젝트에서 써먹는 고급 기법

데코레이터가 뭔지부터 차근차근

📍 데코레이터를 써야 하는 이유

코드를 작성하다 보면 이런 상황이 정말 자주 생기죠:

// 로그인 체크가 필요한 API들...
class UserController {
  getUserProfile() {
    // 매번 이런 코드를 반복해야 함 😱
    if (!this.isAuthenticated()) {
      throw new UnauthorizedException();
    }
    // 실제 로직...
  }

  updateUser() {
    if (!this.isAuthenticated()) {
      throw new UnauthorizedException();
    }
    // 또 반복...
  }

  deleteUser() {
    if (!this.isAuthenticated()) {
      throw new UnauthorizedException();
    }
    // 또또 반복...
  }
}

매번 똑같은 인증 로직을 복사 붙여넣기 하는 게 정말 짜증나잖아요? 데코레이터로 해결하면:

class UserController {
  @RequireAuth()  // 이 한 줄로 끝!
  getUserProfile() {
    // 인증은 데코레이터가 알아서 처리
    // 비즈니스 로직에만 집중
  }

  @RequireAuth()
  updateUser() {
    // 깔끔!
  }

  @RequireAuth()
  deleteUser() {
    // 완전 깔끔!
  }
}

📊 제가 데코레이터 도입 후 체감한 변화:

  • 보일러플레이트 코드 70% 감소
  • 코드 가독성이 눈에 띄게 향상
  • 비즈니스 로직과 부가 기능 완전 분리
  • 재사용 가능한 기능들이 라이브러리처럼 쌓임

🚀 데코레이터의 진짜 정체

데코레이터는 사실 함수예요. 그것도 아주 특별한 함수죠. 클래스나 메서드가 정의될 때 자동으로 실행되면서 원래 코드를 감싸거나 수정할 수 있어요.

// 이렇게 쓰는 데코레이터는...
@MyDecorator()
class MyClass {}

// 사실 이런 함수 호출과 같아요
MyClass = MyDecorator()(MyClass);

신기하죠? 컴파일 타임에 코드를 조작하는 일종의 메타프로그래밍이거든요.

데코레이터 설정 (tsconfig.json):

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "lib": ["ES2020", "DOM"]
  }
}

중요한 주의사항:

  • experimentalDecorators: 현재 TypeScript 데코레이터 기능 활성화
  • emitDecoratorMetadata: reflect-metadata와 함께 사용 시 필수
  • Stage 3 proposal 데코레이터와는 문법이 다름 (현재 실험적 버전 사용 권장)
  • 최신 tslib 설치 권장 (데코레이터 헬퍼 함수 지원)

4가지 데코레이터 타입 완전 해부

TypeScript에는 4가지 종류의 데코레이터가 있어요. 각각 언제 어떻게 쓰는지 실전 예제로 알아볼게요.

1. 클래스 데코레이터 - 클래스 전체를 조작

가장 많이 보는 패턴:

// NestJS의 @Controller 같은 패턴
function Controller(path: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    // 클래스에 메타데이터 추가
    Reflect.defineMetadata('path', path constructor);

    // 원본 클래스를 확장해서 새로운 클래스 반환
    return class extends constructor {
      basePath = path;

      constructor(...args: any[]) {
        super(...args);
        console.log(`Controller created with path: ${path}`);
      }
    };
  };
}

// 사용법
@Controller('/users')
class UserController {
  getUsers() {
    return 'Getting users...';
  }
}

const controller = new UserController();
// "Controller created with path: /users" 출력

2. 메서드 데코레이터 - 메서드를 감싸거나 수정

실무에서 가장 유용한 패턴:

// 실행 시간 측정 데코레이터
function Benchmark(target: any propertyKey: string descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = async function(...args: any[]) {
    const start = Date.now();

    try {
      const result = await originalMethod.apply(this args);
      const end = Date.now();
      console.log(`${propertyKey} 실행 시간: ${end - start}ms`);
      return result;
    } catch (error) {
      const end = Date.now();
      console.log(`${propertyKey} 에러 발생 (${end - start}ms):`, error);
      throw error;
    }
  };
}

// 캐싱 데코레이터
function Cache(ttl: number = 5000) {
  const cache = new Map();

  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
      const key = JSON.stringify(args);

      if (cache.has(key)) {
        console.log(`캐시 히트: ${propertyKey}`);
        return cache.get(key);
      }

      const result = originalMethod.apply(this args);
      cache.set(key result);

      // TTL 후 캐시 삭제
      setTimeout(() => cache.delete(key), ttl);

      return result;
    };
  };
}

class ApiService {
  @Benchmark
  @Cache(10000)
  async fetchUserData(userId: number) {
    // 실제로는 DB 조회나 API 호출
    await new Promise(resolve => setTimeout(resolve 1000));
    return { id: userId name: `User ${userId}` };
  }
}

3. 프로퍼티 데코레이터 - 클래스 속성 관리

// 환경변수 자동 주입
function Env(key: string defaultValue?: string) {
  return function(target: any propertyKey: string) {
    // 프로퍼티 디스크립터 수정
    const getter = () => {
      return process.env[key] || defaultValue;
    };

    Object.defineProperty(target propertyKey, {
      get: getter,
      enumerable: true,
      configurable: true
    });
  };
}

// 유효성 검사 데코레이터
function Required(target: any propertyKey: string) {
  // 메타데이터에 필수 필드 정보 저장
  const existingRequired = Reflect.getMetadata('required', target.constructor) || [];
  Reflect.defineMetadata('required', [...existingRequired propertyKey], target.constructor);
}

class AppConfig {
  @Env('DATABASE_URL', 'localhost:5432')
  databaseUrl: string;

  @Env('API_KEY')
  @Required
  apiKey: string;

  @Env('PORT', '3000')
  port: string;
}

// 유효성 검사 함수
function validateRequired<T>(obj: T): T {
  const constructor = (obj as any).constructor;
  const requiredFields = Reflect.getMetadata('required', constructor) || [];

  for (const field of requiredFields) {
    if (!(obj as any)[field]) {
      throw new Error(`Required field '${field}' is missing`);
    }
  }

  return obj;
}

4. 파라미터 데코레이터 - 메서드 파라미터 처리

// 유효성 검사 파라미터 데코레이터
function ValidateBody(target: any propertyKey: string parameterIndex: number) {
  const existingParams = Reflect.getMetadata('validate-params', target propertyKey) || [];
  existingParams.push({ index: parameterIndex type: 'body' });
  Reflect.defineMetadata('validate-params', existingParams target propertyKey);
}

function ValidateParam(paramName: string) {
  return function(target: any propertyKey: string parameterIndex: number) {
    const existingParams = Reflect.getMetadata('validate-params', target propertyKey) || [];
    existingParams.push({ index: parameterIndex type: 'param', name: paramName });
    Reflect.defineMetadata('validate-params', existingParams target propertyKey);
  };
}

// 실제 유효성 검사를 처리하는 메서드 데코레이터
function ValidateRequest(target: any propertyKey: string descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const paramsToValidate = Reflect.getMetadata('validate-params', target propertyKey) || [];

  descriptor.value = function(...args: any[]) {
    // 파라미터 유효성 검사
    for (const param of paramsToValidate) {
      const value = args[param.index];

      if (param.type === 'body' && (!value || Object.keys(value).length === 0)) {
        throw new Error('Request body is required');
      }

      if (param.type === 'param' && !value) {
        throw new Error(`Parameter '${param.name}' is required`);
      }
    }

    return originalMethod.apply(this args);
  };
}

class UserController {
  @ValidateRequest
  createUser(@ValidateBody userData: any) {
    return `Creating user: ${JSON.stringify(userData)}`;
  }

  @ValidateRequest
  getUser(@ValidateParam('id') userId: string) {
    return `Getting user: ${userId}`;
  }
}

실무에서 바로 쓰는 커스텀 데코레이터

실제 프로젝트에서 정말 유용했던 데코레이터들을 소개할게요.

1. API 응답 표준화 데코레이터

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  message: string;
  timestamp: string;
}

function ApiResponseWrapper(message: string = 'Success') {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function(...args: any[]) {
      try {
        const result = await originalMethod.apply(this args);

        return {
          success: true,
          data: result,
          message,
          timestamp: new Date().toISOString()
        } as ApiResponse<typeof result>;
      } catch (error) {
        return {
          success: false,
          message: error instanceof Error ? error.message : 'Unknown error',
          timestamp: new Date().toISOString()
        } as ApiResponse<null>;
      }
    };
  };
}

class ProductController {
  @ApiResponseWrapper('상품 목록을 성공적으로 조회했습니다')
  async getProducts() {
    // DB에서 상품 조회 로직
    return [
      { id: 1 name: '상품 1' },
      { id: 2 name: '상품 2' }
    ];
  }

  @ApiResponseWrapper('상품이 성공적으로 생성되었습니다')
  async createProduct(productData: any) {
    // 상품 생성 로직
    return { id: 3, ...productData };
  }
}

2. 권한 체크 데코레이터

enum UserRole {
  USER = 'user',
  ADMIN = 'admin',
  SUPER_ADMIN = 'super_admin'
}

function RequireRole(roles: UserRole[]) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
      // 실제로는 JWT 토큰이나 세션에서 사용자 정보를 가져옴
      const currentUser = this.getCurrentUser(); // 예시용 메서드

      if (!currentUser) {
        throw new Error('Authentication required');
      }

      if (!roles.includes(currentUser.role)) {
        throw new Error(`Access denied. Required roles: ${roles.join(', ')}`);
      }

      return originalMethod.apply(this args);
    };
  };
}

class AdminController {
  @RequireRole([UserRole.ADMIN UserRole.SUPER_ADMIN])
  deleteUser(userId: string) {
    return `User ${userId} deleted`;
  }

  @RequireRole([UserRole.SUPER_ADMIN])
  deleteAllUsers() {
    return 'All users deleted';
  }

  getCurrentUser() {
    // 실제로는 JWT 디코딩이나 세션 조회
    return { id: '1', role: UserRole.ADMIN };
  }
}

3. 자동 로깅 데코레이터

enum LogLevel {
  DEBUG = 'debug',
  INFO = 'info',
  WARN = 'warn',
  ERROR = 'error'
}

function Log(level: LogLevel = LogLevel.INFO includeArgs: boolean = false) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const className = target.constructor.name;

    descriptor.value = async function(...args: any[]) {
      const start = Date.now();

      // 메서드 시작 로그
      const logData = {
        timestamp: new Date().toISOString(),
        class: className,
        method: propertyKey,
        level,
        ...(includeArgs && { arguments: args })
      };

      console.log(`[${level.toUpperCase()}] ${className}.${propertyKey} started`, logData);

      try {
        const result = await originalMethod.apply(this args);
        const duration = Date.now() - start;

        // 성공 로그
        console.log(`[${level.toUpperCase()}] ${className}.${propertyKey} completed (${duration}ms)`);
        return result;
      } catch (error) {
        const duration = Date.now() - start;

        // 에러 로그
        console.error(`[ERROR] ${className}.${propertyKey} failed (${duration}ms):`, error);
        throw error;
      }
    };
  };
}

class OrderService {
  @Log(LogLevel.INFO true)
  async createOrder(orderData: any) {
    // 주문 생성 로직
    await new Promise(resolve => setTimeout(resolve 100)); // 시뮬레이션
    return { orderId: '12345', ...orderData };
  }

  @Log(LogLevel.WARN)
  async cancelOrder(orderId: string) {
    // 주문 취소 로직
    return `Order ${orderId} cancelled`;
  }
}

NestJS 스타일 고급 패턴 마스터

NestJS에서 사용하는 데코레이터 패턴을 분석해보고 비슷한 기능을 직접 만들어볼게요.

의존성 주입 데코레이터 만들기

// 간단한 DI 컨테이너
class DIContainer {
  private static services = new Map<string any>();
  private static instances = new Map<string any>();

  static register<T>(token: string service: new(...args: any[]) => T) {
    this.services.set(token service);
  }

  static get<T>(token: string): T {
    if (this.instances.has(token)) {
      return this.instances.get(token);
    }

    const ServiceClass = this.services.get(token);
    if (!ServiceClass) {
      throw new Error(`Service ${token} not found`);
    }

    const instance = new ServiceClass();
    this.instances.set(token instance);
    return instance;
  }
}

// @Injectable 데코레이터
function Injectable(token?: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const serviceToken = token || constructor.name;
    DIContainer.register(serviceToken constructor);

    return constructor;
  };
}

// @Inject 데코레이터
function Inject(token: string) {
  return function(target: any propertyKey: string) {
    Object.defineProperty(target propertyKey, {
      get() {
        return DIContainer.get(token);
      },
      enumerable: true,
      configurable: true
    });
  };
}

// 사용 예시
@Injectable('UserService')
class UserService {
  getUser(id: string) {
    return { id name: `User ${id}` };
  }
}

@Injectable('EmailService')
class EmailService {
  sendEmail(to: string subject: string) {
    console.log(`Sending email to ${to}: ${subject}`);
  }
}

class UserController {
  @Inject('UserService')
  private userService: UserService;

  @Inject('EmailService')
  private emailService: EmailService;

  createUser(userData: any) {
    const user = this.userService.getUser('123');
    this.emailService.sendEmail(user.name, 'Welcome!');
    return user;
  }
}

라우팅 데코레이터 시스템

// 간단한 라우팅 시스템
interface Route {
  method: string;
  path: string;
  handler: string;
  middlewares: Function[];
}

class Router {
  private static routes: Route[] = [];

  static addRoute(route: Route) {
    this.routes.push(route);
  }

  static getRoutes() {
    return this.routes;
  }

  static printRoutes() {
    console.log('Registered routes:');
    this.routes.forEach(route => {
      console.log(`${route.method.toUpperCase()} ${route.path} -> ${route.handler}`);
    });
  }
}

// HTTP 메서드 데코레이터들
function Get(path: string) {
  return function(target: any propertyKey: string) {
    Router.addRoute({
      method: 'GET',
      path,
      handler: `${target.constructor.name}.${propertyKey}`,
      middlewares: []
    });
  };
}

function Post(path: string) {
  return function(target: any propertyKey: string) {
    Router.addRoute({
      method: 'POST',
      path,
      handler: `${target.constructor.name}.${propertyKey}`,
      middlewares: []
    });
  };
}

// 미들웨어 데코레이터
function UseMiddleware(...middlewares: Function[]) {
  return function(target: any propertyKey: string) {
    const routes = Router.getRoutes();
    const lastRoute = routes[routes.length - 1];
    if (lastRoute && lastRoute.handler.includes(propertyKey)) {
      lastRoute.middlewares.push(...middlewares);
    }
  };
}

// 인증 미들웨어 예시
const authMiddleware = (req: any res: any next: any) => {
  console.log('Auth middleware executed');
  next();
};

const logMiddleware = (req: any res: any next: any) => {
  console.log('Log middleware executed');
  next();
};

class ApiController {
  @Get('/users')
  @UseMiddleware(authMiddleware logMiddleware)
  getUsers() {
    return 'Getting users...';
  }

  @Post('/users')
  @UseMiddleware(authMiddleware)
  createUser() {
    return 'Creating user...';
  }

  @Get('/health')
  healthCheck() {
    return 'OK';
  }
}

// 라우트 등록 확인
const controller = new ApiController();
Router.printRoutes();

메타데이터와 reflect-metadata 활용법

더 고급 데코레이터를 만들려면 메타데이터 API를 잘 알아야 해요.

reflect-metadata 설치 및 설정

npm install reflect-metadata
npm install -D @types/reflect-metadata

# 최신 tslib도 함께 설치 (데코레이터 헬퍼 함수 지원)
npm install tslib
// main.ts 최상단에 import
import 'reflect-metadata';

// TypeScript 5.0+ 사용 시 추가 설정 필요없음

메타데이터 기반 유효성 검사 시스템

// 유효성 검사 데코레이터들
function IsRequired(target: any propertyKey: string) {
  Reflect.defineMetadata('isRequired', true target propertyKey);
}

function IsString(target: any propertyKey: string) {
  Reflect.defineMetadata('type', 'string', target propertyKey);
}

function IsNumber(target: any propertyKey: string) {
  Reflect.defineMetadata('type', 'number', target propertyKey);
}

function MinLength(length: number) {
  return function(target: any propertyKey: string) {
    Reflect.defineMetadata('minLength', length target propertyKey);
  };
}

function MaxLength(length: number) {
  return function(target: any propertyKey: string) {
    Reflect.defineMetadata('maxLength', length target propertyKey);
  };
}

// 유효성 검사 클래스 정의
class CreateUserDTO {
  @IsRequired
  @IsString
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @IsRequired
  @IsString
  @MinLength(5)
  email: string;

  @IsNumber
  age?: number;
}

// 유효성 검사 함수
function validate<T>(obj: T): { isValid: boolean; errors: string[] } {
  const errors: string[] = [];

  const keys = Object.keys(obj as any);

  for (const key of keys) {
    const value = (obj as any)[key];

    // Required 체크
    if (Reflect.getMetadata('isRequired', obj key) && (value === undefined || value === null)) {
      errors.push(`${key} is required`);
      continue;
    }

    // 값이 없으면 다른 검사는 스킵
    if (value === undefined || value === null) continue;

    // 타입 체크
    const expectedType = Reflect.getMetadata('type', obj key);
    if (expectedType && typeof value !== expectedType) {
      errors.push(`${key} must be ${expectedType}`);
    }

    // 문자열 길이 체크
    if (typeof value === 'string') {
      const minLength = Reflect.getMetadata('minLength', obj key);
      if (minLength && value.length < minLength) {
        errors.push(`${key} must be at least ${minLength} characters long`);
      }

      const maxLength = Reflect.getMetadata('maxLength', obj key);
      if (maxLength && value.length > maxLength) {
        errors.push(`${key} must be at most ${maxLength} characters long`);
      }
    }
  }

  return {
    isValid: errors.length === 0,
    errors
  };
}

// Body 유효성 검사 데코레이터
function ValidateBody<T>(dtoClass: new() => T) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
      const bodyData = args[0]; // 첫 번째 인자가 body라고 가정

      // DTO 인스턴스 생성
      const dto = Object.assign(new dtoClass(), bodyData);

      // 유효성 검사
      const validation = validate(dto);

      if (!validation.isValid) {
        throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
      }

      // 검증된 DTO를 인자로 전달
      args[0] = dto;
      return originalMethod.apply(this args);
    };
  };
}

// 사용 예시
class UserController {
  @ValidateBody(CreateUserDTO)
  createUser(userData: CreateUserDTO) {
    console.log('Creating user with validated data:', userData);
    return { id: '123', ...userData };
  }
}

// 테스트
const controller = new UserController();

try {
  const result = controller.createUser({
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
  } as any);
  console.log('Success:', result);
} catch (error) {
  console.error('Validation error:', error.message);
}

실전 프로젝트에서 써먹는 고급 기법

1. 데코레이터 합성(Composition)

여러 데코레이터를 조합해서 더 강력한 기능을 만들어보죠:

// 여러 데코레이터를 한 번에 적용하는 유틸리티
function compose(...decorators: any[]) {
  return function(target: any propertyKey?: string descriptor?: PropertyDescriptor) {
    decorators.reverse().forEach(decorator => {
      if (typeof decorator === 'function') {
        if (propertyKey && descriptor) {
          decorator(target propertyKey descriptor);
        } else {
          decorator(target);
        }
      }
    });
  };
}

// 자주 사용하는 데코레이터 조합을 미리 정의
const SecureApiEndpoint = compose(
  RequireRole([UserRole.ADMIN]),
  Log(LogLevel.INFO true),
  Benchmark,
  ApiResponseWrapper('Operation completed successfully')
);

class SecureController {
  @SecureApiEndpoint
  sensitiveOperation(data: any) {
    return `Processing sensitive data: ${JSON.stringify(data)}`;
  }
}

2. 조건부 데코레이터

환경이나 설정에 따라 다르게 동작하는 데코레이터:

function ConditionalCache(condition: () => boolean ttl: number = 5000) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    if (!condition()) {
      // 조건이 맞지 않으면 데코레이터를 적용하지 않음
      return;
    }

    // 조건이 맞으면 캐시 데코레이터 적용
    Cache(ttl)(target propertyKey descriptor);
  };
}

function ConditionalLog(condition: () => boolean level: LogLevel = LogLevel.INFO) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    if (!condition()) {
      return;
    }

    Log(level true)(target propertyKey descriptor);
  };
}

class ProductService {
  @ConditionalCache(() => process.env.NODE_ENV === 'production', 10000)
  @ConditionalLog(() => process.env.NODE_ENV === 'development', LogLevel.DEBUG)
  async getProducts() {
    // 프로덕션에서는 캐시 적용 개발에서는 로깅 적용
    return ['Product 1', 'Product 2'];
  }
}

3. 타입 안전한 데코레이터 팩토리

// 타입 안전한 데코레이터 인터페이스
interface DecoratorOptions {
  cache?: { ttl: number };
  auth?: { roles: UserRole[] };
  log?: { level: LogLevel; includeArgs: boolean };
  validate?: { bodyType?: new() => any; paramTypes?: Record<string string> };
}

function ApiEndpoint<T extends DecoratorOptions>(options: T) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    // 옵션에 따라 다른 데코레이터들을 적용
    if (options.cache) {
      Cache(options.cache.ttl)(target propertyKey descriptor);
    }

    if (options.auth) {
      RequireRole(options.auth.roles)(target propertyKey descriptor);
    }

    if (options.log) {
      Log(options.log.level options.log.includeArgs)(target propertyKey descriptor);
    }

    if (options.validate?.bodyType) {
      ValidateBody(options.validate.bodyType)(target propertyKey descriptor);
    }

    // 항상 응답을 래핑
    ApiResponseWrapper()(target propertyKey descriptor);
  };
}

// 타입 안전한 사용
class ModernController {
  @ApiEndpoint({
    auth: { roles: [UserRole.ADMIN] },
    log: { level: LogLevel.INFO includeArgs: true },
    cache: { ttl: 30000 }
  })
  getAdminData() {
    return { adminData: 'sensitive information' };
  }

  @ApiEndpoint({
    validate: { bodyType: CreateUserDTO },
    log: { level: LogLevel.DEBUG includeArgs: false }
  })
  createUser(userData: CreateUserDTO) {
    return { created: true user: userData };
  }
}

💡 실무 활용 꿀팁

데코레이터 성능 최적화

// 메모이제이션을 활용한 데코레이터 최적화
const decoratorCache = new Map();

function MemoizedDecorator(key: string) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const cacheKey = `${target.constructor.name}.${propertyKey}.${key}`;

    if (decoratorCache.has(cacheKey)) {
      descriptor.value = decoratorCache.get(cacheKey);
      return;
    }

    const originalMethod = descriptor.value;
    const memoizedMethod = function(...args: any[]) {
      // 실제 로직
      return originalMethod.apply(this args);
    };

    decoratorCache.set(cacheKey memoizedMethod);
    descriptor.value = memoizedMethod;
  };
}

에러 처리 전략

function SafeExecute(fallbackValue?: any) {
  return function(target: any propertyKey: string descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function(...args: any[]) {
      try {
        return await originalMethod.apply(this args);
      } catch (error) {
        console.error(`Error in ${target.constructor.name}.${propertyKey}:`, error);

        // 에러 로깅 모니터링 등
        if (typeof window !== 'undefined' && (window as any).Sentry) {
          (window as any).Sentry.captureException(error);
        }

        return fallbackValue;
      }
    };
  };
}

class RobustService {
  @SafeExecute([])
  async getDataWithFallback() {
    // 실패할 수 있는 로직
    throw new Error('Network error');
  }
}

자주 묻는 질문 (FAQ)

Q1: 데코레이터는 언제 실행되나요?

A: 클래스가 정의되는 시점(컴파일 타임)에 실행돼요. 런타임이 아닌 클래스 선언 시에 바로 동작합니다.

Q2: 데코레이터가 성능에 영향을 주나요?

A: 데코레이터 자체는 컴파일 타임에 실행되므로 런타임 성능에는 크게 영향 없어요. 내부 로직에 따라 차이가 있을 수 있지만요.

Q3: 여러 데코레이터를 함께 사용할 때 실행 순서는?

A: 아래에서 위로(bottom-up) 실행됩니다. @A @B method()에서 B가 먼저 A가 나중에 실행돼요.

Q4: 프로덕션에서 사용해도 안전한가요?

A: NestJS 같은 프레임워워크에서 널리 사용되고 있어서 안정적이에요. 다만 아직 실험적 기능이라 주의는 필요해요.

Q5: 데코레이터 없이는 같은 기능을 구현할 수 없나요?

A: 구현할 수는 있지만 코드가 복잡하고 재사용성이 떨어져요. 데코레이터의 진짜 가치는 깔끔함과 재사용성이거든요.

❓ TypeScript 데코레이터 마스터 마무리

데코레이터는 처음에는 마법 같아 보이지만 원리를 이해하면 정말 강력한 도구가 돼요. 특히 반복되는 코드를 줄이고 비즈니스 로직에 집중할 수 있게 해주는 게 가장 큰 장점이에요.

실무에서 써보면 코드가 훨씬 깔끔해지고 유지보수도 쉬워집니다. NestJS나 Angular 같은 프레임워크를 쓸 때도 이제 @Controller, @Injectable 같은 게 왜 있는지 이해가 되실 거예요!

데코레이터로 메타프로그래밍의 세계를 경험해보세요. 코드가 코드를 만드는 재미를 느낄 수 있을 거예요! 💪

🔗 TypeScript 심화 학습 시리즈

데코레이터를 마스터하셨다면 다른 고급 TypeScript 기능들도 함께 학습해보세요:

📚 다음 단계 학습 가이드

📚 공식 문서 및 참고 자료