본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: TC39 표준 데코레이터 완벽 이해
2026년 1월 26일·프로그래밍·

3장: TC39 표준 데코레이터 완벽 이해

TypeScript 5.0에서 도입된 TC39 Stage 3 데코레이터의 원리, API 구조, 실전 패턴을 다루고, 기존 실험적 데코레이터와의 차이를 분석합니다.

14분697자8개 섹션
typescriptperformancedevtools
공유
typescript-deepdive3 / 12
123456789101112
이전2장: const 타입 파라미터와 satisfies 연산자다음4장: using 선언과 명시적 리소스 관리

2장에서 const 타입 파라미터와 satisfies로 타입 추론을 제어하는 방법을 배웠습니다. 이 장에서는 TypeScript 5.0의 또 다른 핵심 변경인 TC39 표준 데코레이터를 다룹니다. 데코레이터는 클래스와 그 멤버를 선언적으로 수정하는 강력한 메타프로그래밍 도구입니다.

두 가지 데코레이터: 실험적 vs TC39 표준

TypeScript에는 현재 두 가지 데코레이터 시스템이 공존합니다.

구분실험적 데코레이터TC39 표준 데코레이터
도입 시기TypeScript 1.5 (2015)TypeScript 5.0 (2023)
활성화experimentalDecorators: true기본 활성 (5.0+)
표준TypeScript 독자 제안TC39 Stage 3
런타임 호환TypeScript 전용JavaScript 엔진 네이티브 지원 예정
매개변수(target, key, descriptor)(target, context)
적용 대상클래스, 메서드, 접근자, 속성, 매개변수클래스, 메서드, 접근자, 필드, getter/setter, auto-accessor
Warning

tsconfig.json에 experimentalDecorators: true가 설정되어 있으면 기존 실험적 데코레이터가 활성화됩니다. TC39 표준 데코레이터를 사용하려면 이 옵션을 제거하거나 false로 설정해야 합니다. 두 시스템은 동시에 사용할 수 없습니다.

TC39 데코레이터의 구조

데코레이터 함수의 시그니처

TC39 데코레이터는 두 개의 인수를 받습니다.

데코레이터 시그니처
typescript
type Decorator = (
  target: any,           // 데코레이팅되는 값 (메서드 함수, 클래스 등)
  context: DecoratorContext  // 데코레이터에 대한 메타 정보
) => any | void;         // 대체할 값을 반환하거나 void

context 객체는 데코레이터가 적용된 위치에 따라 다른 프로퍼티를 제공합니다.

DecoratorContext 공통 프로퍼티
typescript
interface DecoratorContext {
  kind: "class" | "method" | "getter" | "setter" | "field" | "accessor";
  name: string | symbol;
  static: boolean;           // 정적 멤버 여부
  private: boolean;          // 프라이빗 멤버 여부
  access: {                  // 접근자 함수
    get?(): unknown;
    set?(value: unknown): void;
    has?(value: unknown): boolean;
  };
  addInitializer(fn: () => void): void;  // 초기화 로직 추가
  metadata: Record<string | symbol, unknown>;  // 5.2+ 메타데이터
}

데코레이터 종류별 실전 예제

클래스 데코레이터

클래스 데코레이터는 클래스 자체를 수정하거나 대체할 수 있습니다.

클래스 데코레이터
typescript
function sealed(target: Function, context: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
}
 
function withTimestamp<T extends new (...args: any[]) => any>(
  target: T,
  context: ClassDecoratorContext
) {
  return class extends target {
    createdAt = new Date();
  };
}
 
@sealed
@withTimestamp
class User {
  constructor(public name: string) {}
}
 
const user = new User("Kreath");
console.log((user as any).createdAt); // Date 인스턴스

addInitializer를 사용하면 클래스 정의 완료 후 실행할 로직을 등록할 수 있습니다.

addInitializer 활용
typescript
function register(target: Function, context: ClassDecoratorContext) {
  context.addInitializer(function () {
    // 클래스 정의가 완료된 후 실행
    globalRegistry.set(context.name, target);
  });
}

메서드 데코레이터

메서드 데코레이터는 가장 자주 사용되는 유형으로, 메서드의 동작을 감싸거나 수정합니다.

메서드 데코레이터 — 로깅
typescript
function log(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);
 
  function replacement(this: any, ...args: any[]) {
    console.log(`[${methodName}] called with:`, args);
    const result = target.call(this, ...args);
    console.log(`[${methodName}] returned:`, result);
    return result;
  }
 
  return replacement;
}
 
class MathService {
  @log
  multiply(a: number, b: number) {
    return a * b;
  }
}
 
const math = new MathService();
math.multiply(3, 4);
// [multiply] called with: [3, 4]
// [multiply] returned: 12

메서드 데코레이터 — 디바운스

디바운스 데코레이터
typescript
function debounce(ms: number) {
  return function (
    target: Function,
    context: ClassMethodDecoratorContext
  ) {
    let timer: ReturnType<typeof setTimeout>;
 
    function replacement(this: any, ...args: any[]) {
      clearTimeout(timer);
      timer = setTimeout(() => target.apply(this, args), ms);
    }
 
    return replacement;
  };
}
 
class SearchController {
  @debounce(300)
  onInput(query: string) {
    console.log(`Searching for: ${query}`);
  }
}
Tip

데코레이터 팩토리(인수를 받는 데코레이터)는 데코레이터를 반환하는 함수입니다. @debounce(300)에서 debounce(300)이 실제 데코레이터를 반환합니다.

필드 데코레이터

필드 데코레이터는 필드의 초기값을 변환할 수 있습니다.

필드 데코레이터
typescript
function uppercase(
  target: undefined,  // 필드 데코레이터에서 target은 항상 undefined
  context: ClassFieldDecoratorContext
) {
  return function (initialValue: string) {
    return initialValue.toUpperCase();
  };
}
 
function range(min: number, max: number) {
  return function (
    target: undefined,
    context: ClassFieldDecoratorContext
  ) {
    return function (initialValue: number) {
      return Math.max(min, Math.min(max, initialValue));
    };
  };
}
 
class Settings {
  @uppercase
  locale = "en-us";
 
  @range(0, 100)
  volume = 150;  // 100으로 클램핑됨
}
 
const settings = new Settings();
console.log(settings.locale);  // "EN-US"
console.log(settings.volume);  // 100

Auto-Accessor 데코레이터

TC39 표준 데코레이터에서 새로 도입된 accessor 키워드는 getter/setter를 자동 생성합니다. 이를 통해 필드의 읽기/쓰기를 가로챌 수 있습니다.

Auto-Accessor 데코레이터
typescript
function observable(
  target: ClassAccessorDecoratorTarget<any, any>,
  context: ClassAccessorDecoratorContext
) {
  return {
    get(this: any) {
      return target.get.call(this);
    },
    set(this: any, value: any) {
      const oldValue = target.get.call(this);
      target.set.call(this, value);
      if (oldValue !== value) {
        console.log(`${String(context.name)} changed: ${oldValue} -> ${value}`);
      }
    },
    init(initialValue: any) {
      return initialValue;
    },
  };
}
 
class Store {
  @observable
  accessor count = 0;
 
  @observable
  accessor name = "untitled";
}
 
const store = new Store();
store.count = 5;  // "count changed: 0 -> 5"
store.name = "my-store";  // "name changed: untitled -> my-store"

데코레이터 메타데이터 (5.2+)

TypeScript 5.2에서 추가된 데코레이터 메타데이터를 사용하면, 데코레이터가 클래스에 메타데이터를 부착하고 이를 런타임에 읽을 수 있습니다.

데코레이터 메타데이터
typescript
const VALIDATORS = Symbol("validators");
 
function validate(schema: { type: string; min?: number; max?: number }) {
  return function (
    target: undefined,
    context: ClassFieldDecoratorContext
  ) {
    // 메타데이터에 검증 규칙 저장
    const existing = (context.metadata[VALIDATORS] as any[]) || [];
    existing.push({ field: context.name, ...schema });
    context.metadata[VALIDATORS] = existing;
  };
}
 
class UserDTO {
  @validate({ type: "string", min: 1, max: 50 })
  name = "";
 
  @validate({ type: "number", min: 0, max: 150 })
  age = 0;
 
  @validate({ type: "string", min: 5 })
  email = "";
}
 
// 런타임에 메타데이터 읽기
const metadata = UserDTO[Symbol.metadata];
const validators = metadata?.[VALIDATORS];
console.log(validators);
// [
//   { field: "name", type: "string", min: 1, max: 50 },
//   { field: "age", type: "number", min: 0, max: 150 },
//   { field: "email", type: "string", min: 5 }
// ]
Info

메타데이터는 Symbol.metadata를 통해 클래스에 부착됩니다. 이를 활용하면 DI(의존성 주입), ORM 매핑, 검증 등의 프레임워크를 구축할 수 있습니다.

실전 패턴: 의존성 주입 컨테이너

데코레이터와 메타데이터를 결합하면 간단한 DI 컨테이너를 구축할 수 있습니다.

간단한 DI 컨테이너
typescript
const INJECTABLE = Symbol("injectable");
const container = new Map<string, any>();
 
function injectable(
  target: new (...args: any[]) => any,
  context: ClassDecoratorContext
) {
  const name = String(context.name);
  context.addInitializer(function () {
    container.set(name, new target());
  });
}
 
function inject(serviceName: string) {
  return function (
    target: undefined,
    context: ClassFieldDecoratorContext
  ) {
    return function () {
      return container.get(serviceName);
    };
  };
}
 
@injectable
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}
 
@injectable
class UserService {
  @inject("Logger")
  private logger!: Logger;
 
  createUser(name: string) {
    this.logger.log(`Creating user: ${name}`);
  }
}

데코레이터 합성

여러 데코레이터를 하나의 대상에 적용할 때, 실행 순서는 아래에서 위로(오른쪽에서 왼쪽으로) 진행됩니다. 이는 수학의 함수 합성 f(g(x))와 같습니다.

데코레이터 합성 순서
typescript
function first(target: Function, context: ClassMethodDecoratorContext) {
  console.log("first 평가");
  return function (this: any, ...args: any[]) {
    console.log("first 실행");
    return target.call(this, ...args);
  };
}
 
function second(target: Function, context: ClassMethodDecoratorContext) {
  console.log("second 평가");
  return function (this: any, ...args: any[]) {
    console.log("second 실행");
    return target.call(this, ...args);
  };
}
 
class Example {
  @first
  @second
  greet() {
    return "hello";
  }
}
 
// 평가(데코레이터 함수 호출) 순서: second → first
// 실행(래핑된 함수 호출) 순서: first → second → 원본

기존 코드 마이그레이션

실험적 데코레이터에서 TC39 데코레이터로

주요 차이점을 정리하면 다음과 같습니다.

마이그레이션 비교
typescript
// === 실험적 데코레이터 ===
function logLegacy(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`);
    return original.apply(this, args);
  };
}
 
// === TC39 표준 데코레이터 ===
function logStandard(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  const name = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${name}`);
    return target.call(this, ...args);
  };
}

핵심 차이는 다음과 같습니다.

  1. PropertyDescriptor 제거 — TC39 데코레이터는 descriptor 대신 값을 직접 받고 반환합니다
  2. context 객체 도입 — propertyKey 대신 context.name, 정적/프라이빗 여부 등 풍부한 정보 제공
  3. 매개변수 데코레이터 미지원 — TC39 표준에는 매개변수 데코레이터가 없습니다. NestJS 등에서 사용하는 @Body(), @Param() 등은 별도의 전략이 필요합니다
Warning

NestJS, Angular 등 실험적 데코레이터에 깊이 의존하는 프레임워크는 아직 TC39 표준으로의 전환이 진행 중입니다. 프레임워크의 공식 마이그레이션 가이드를 확인하고 전환하세요.

정리

TC39 표준 데코레이터는 TypeScript의 메타프로그래밍을 JavaScript 표준 위에 올려놓는 중요한 변화입니다. 클래스, 메서드, 필드, auto-accessor에 적용할 수 있으며, context 객체를 통해 풍부한 메타 정보에 접근할 수 있습니다. 데코레이터 메타데이터(5.2+)와 결합하면 DI 컨테이너, ORM, 검증 시스템 등 강력한 프레임워크 기반을 구축할 수 있습니다.

다음 장에서는 TypeScript 5.2에서 도입된 using 선언과 명시적 리소스 관리를 살펴봅니다. 파일 핸들, 데이터베이스 연결 등의 리소스를 안전하게 해제하는 새로운 패턴을 배웁니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#typescript#performance#devtools

관련 글

프로그래밍

4장: using 선언과 명시적 리소스 관리

TypeScript 5.2에서 도입된 using 선언과 Symbol.dispose를 활용한 명시적 리소스 관리 패턴을 실전 예제와 함께 심층 분석합니다.

2026년 1월 28일·12분
프로그래밍

2장: const 타입 파라미터와 satisfies 연산자

TypeScript의 타입 추론을 정밀하게 제어하는 두 가지 핵심 도구인 const 타입 파라미터와 satisfies 연산자의 원리, 차이점, 실전 활용 패턴을 다룹니다.

2026년 1월 24일·15분
프로그래밍

5장: 조건부 타입 심층 분석

TypeScript 조건부 타입의 원리, 분배적 조건부 타입, infer 키워드와의 조합, 그리고 실전 활용 패턴을 깊이 있게 다룹니다.

2026년 1월 30일·13분
이전 글2장: const 타입 파라미터와 satisfies 연산자
다음 글4장: using 선언과 명시적 리소스 관리

댓글

목차

약 14분 남음
  • 두 가지 데코레이터: 실험적 vs TC39 표준
  • TC39 데코레이터의 구조
    • 데코레이터 함수의 시그니처
  • 데코레이터 종류별 실전 예제
    • 클래스 데코레이터
    • 메서드 데코레이터
    • 메서드 데코레이터 — 디바운스
    • 필드 데코레이터
    • Auto-Accessor 데코레이터
  • 데코레이터 메타데이터 (5.2+)
  • 실전 패턴: 의존성 주입 컨테이너
  • 데코레이터 합성
  • 기존 코드 마이그레이션
    • 실험적 데코레이터에서 TC39 데코레이터로
  • 정리