Published on

TS 5.6 데코레이터 문법 전환으로 컴파일 오류 해결

Authors

서버/백엔드(TypeORM, NestJS, class-validator)든 프론트(Angular)든, 데코레이터는 “한 번 잡히면 생산성이 확 올라가는” 문법입니다. 문제는 TS 5.6으로 올리면서 데코레이터 관련 컴파일 오류가 갑자기 터지는 팀이 많다는 점입니다. 원인은 단순히 “타입스크립트가 버그”라기보다, 데코레이터가 실험(experimental) 단계에서 표준(ECMAScript) 방향으로 정리되며 타입/런타임 계약이 달라졌기 때문인 경우가 대부분입니다.

이 글에서는 TS 5.6에서 자주 마주치는 데코레이터 오류의 패턴을 분해하고, tsconfig 설정 정리 → 코드 전환(또는 유지) → 라이브러리 호환성 체크 순서로 안전하게 해결하는 방법을 다룹니다. (타입 안정성 강화로 인한 오류라면 TypeScript 5.5+ noUncheckedIndexedAccess 오류 실전해결도 함께 참고하면 좋습니다.)

1) TS 5.6에서 데코레이터가 왜 문제가 되나

데코레이터는 크게 두 계열이 공존해 왔습니다.

  • Legacy/Experimental 데코레이터: experimentalDecorators: true 기반. 오래된 Stage 2 초안에 가까운 동작(특히 메서드/프로퍼티 데코레이터 시그니처)이 널리 퍼져 있음.
  • 표준(ECMAScript) 데코레이터: 최신 TC39 제안(사실상 표준화된 형태)에 맞춘 동작. 데코레이터 함수가 받는 인자(컨텍스트)가 달라지고, 반환 규칙도 더 엄격해짐.

TS 5.6 업그레이드 시 문제가 되는 지점은 보통 다음 중 하나입니다.

  1. tsconfig에서 어떤 데코레이터 모델을 쓰는지 모호하거나, 라이브러리/빌드툴이 기대하는 모델과 달라짐
  2. 기존 데코레이터 구현이 legacy 시그니처에 고정되어 있어 표준 컨텍스트를 못 받음
  3. emitDecoratorMetadata/reflect-metadata 의존(특히 NestJS/TypeORM)과 표준 데코레이터의 철학이 충돌

즉, “TS 5.6이 데코레이터를 바꿨다”가 아니라, 프로젝트가 어떤 데코레이터 계약을 전제로 작성됐는지를 먼저 확정해야 합니다.

2) 가장 흔한 컴파일 오류 패턴

프로젝트마다 메시지는 조금씩 다르지만, 아래 유형으로 수렴합니다.

2.1 데코레이터 시그니처 불일치

Legacy 방식 데코레이터는 보통 이렇게 작성되어 있습니다.

// legacy 스타일(예: 메서드 데코레이터)
export function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log("call", propertyKey, args);
    return original.apply(this, args);
  };
}

TS 5.6에서 표준 데코레이터로 해석되는 환경이라면, 위 함수는 인자 개수/형태가 맞지 않아 오류가 납니다. 표준 데코레이터는 대략 다음 형태를 기대합니다.

// 표준(ECMAScript) 스타일(메서드 데코레이터)
export function Log(value: Function, context: ClassMethodDecoratorContext) {
  return function (this: any, ...args: any[]) {
    console.log("call", String(context.name), args);
    return value.apply(this, args);
  };
}

핵심 차이:

  • target/propertyKey/descriptor(value, context)
  • 반환값이 “수정된 함수/초기화 함수 등”으로 명확해짐

2.2 reflect-metadata 기반 메타데이터가 사라짐

NestJS/TypeORM/class-validator 계열은 종종 다음을 전제로 합니다.

  • emitDecoratorMetadata: true
  • 런타임에서 Reflect.getMetadata("design:type", ...) 같은 호출

표준 데코레이터는 기본 철학이 “런타임 메타데이터를 자동으로 뿌리지 않는다”에 가깝고, TS의 emitDecoratorMetadata는 legacy 흐름과 결합된 경우가 많습니다. 그래서 업그레이드 후에 메타데이터가 기대대로 생기지 않거나, 타입이 달라져 오류가 날 수 있습니다.

2.3 빌드 체인(바벨/ts-jest/ts-node/번들러) 불일치

TypeScript 컴파일러(tsc)는 설정에 따라 데코레이터를 처리하지만, 실제 프로젝트는 다음이 섞입니다.

  • Jest: ts-jest / babel-jest
  • 런타임: ts-node / tsx
  • 번들러: swc / esbuild / babel

이 중 하나가 legacy 데코레이터를 가정하고 다른 하나가 표준 데코레이터를 가정하면, “컴파일은 되는데 런타임이 깨짐” 또는 “빌드만 실패”가 발생합니다.

3) 먼저 결론: ‘전환’ vs ‘유지’ 선택 기준

TS 5.6에서 데코레이터 오류를 해결하는 가장 빠른 길은 둘 중 하나를 명확히 고르는 것입니다.

3.1 legacy(기존) 데코레이터를 유지해야 하는 경우

  • NestJS/TypeORM 등 reflect-metadata + emitDecoratorMetadata에 강하게 의존
  • 이미 수십/수백 개 데코레이터가 legacy 시그니처로 구현
  • 표준 데코레이터 지원이 라이브러리/툴체인에서 아직 불안정

이 경우는 “TS 5.6에서도 legacy 데코레이터로 고정”하는 전략이 현실적입니다.

3.2 표준(ECMAScript) 데코레이터로 전환하는 게 좋은 경우

  • 자체 데코레이터(사내 프레임워크) 비중이 높고, 런타임 메타데이터 의존이 적음
  • 최신 번들러/런타임(특히 ESM, 최신 Node) 위주로 정리하려는 상황
  • 장기적으로 표준 스펙을 따라가고 싶은 경우

Node/ESM 전환이 함께 진행 중이라면, 데코레이터 이슈와 맞물려 빌드 체인이 더 흔들릴 수 있습니다. ESM 전환이 필요하다면 Node 22에서 require가 안 될 때 ESM 전환법도 같이 점검하세요.

4) tsconfig에서 가장 중요한 체크 포인트

여기서 목표는 “우리 프로젝트가 어떤 데코레이터 모델을 쓰는지”를 컴파일러와 툴체인 모두에게 일관되게 선언하는 것입니다.

4.1 legacy 데코레이터 유지(권장: Nest/TypeORM 계열)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": false,
    "strict": true
  }
}

설명:

  • experimentalDecorators: true: legacy 시그니처를 유지하는 핵심
  • emitDecoratorMetadata: true: reflect-metadata 기반 라이브러리 호환
  • useDefineForClassFields: false: 필드 초기화 타이밍 차이로 데코레이터/ORM이 깨지는 케이스가 있어 보수적으로 둠(특히 구형 라이브러리)

4.2 표준 데코레이터로 전환(자체 데코레이터 중심)

표준 데코레이터는 TS 버전/설정 조합에 따라 옵션이 달라질 수 있으니, 핵심은 다음입니다.

  • legacy 옵션(experimentalDecorators, emitDecoratorMetadata)에 의존하지 않기
  • 데코레이터 구현을 (value, context) 기반으로 바꾸기
  • 빌드툴(swc/babel)도 동일한 데코레이터 변환 모드인지 확인하기

예시(개념적으로):

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true
  }
}

표준 데코레이터는 “설정 한 줄”로 끝나기보다는 코드와 툴체인 전체를 표준 모델로 맞추는 작업이 함께 필요합니다.

5) 코드 전환 실전: legacy → 표준 패턴

여기서는 자주 쓰는 3가지(메서드/필드/클래스) 패턴을 전환 예제로 정리합니다.

5.1 메서드 데코레이터: descriptor 조작 → 함수 래핑 반환

before (legacy)

export function Time(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const t0 = performance.now();
    try {
      return original.apply(this, args);
    } finally {
      const t1 = performance.now();
      console.log(`${propertyKey}: ${(t1 - t0).toFixed(2)}ms`);
    }
  };
}

after (표준)

export function Time(value: Function, context: ClassMethodDecoratorContext) {
  return function (this: any, ...args: any[]) {
    const t0 = performance.now();
    try {
      return value.apply(this, args);
    } finally {
      const t1 = performance.now();
      console.log(`${String(context.name)}: ${(t1 - t0).toFixed(2)}ms`);
    }
  };
}

포인트:

  • descriptor.value를 바꾸는 대신, 새 함수를 반환
  • 메서드 이름은 context.name로 접근

5.2 필드 데코레이터: 초기화 훅 사용

legacy에서 프로퍼티 데코레이터는 targetpropertyKey만 받아서 메타데이터만 심는 식이 많았습니다.

표준에서는 필드/접근자에 대해 초기화 시점에 개입할 수 있습니다.

export function Default<T>(defaultValue: T) {
  return function (_: undefined, context: ClassFieldDecoratorContext) {
    return function (initialValue: T) {
      return initialValue ?? defaultValue;
    };
  };
}

class User {
  @Default("guest")
  role!: string;
}

이런 식으로 “값 보정”을 데코레이터로 안전하게 넣을 수 있습니다.

5.3 클래스 데코레이터: 생성자 래핑

export function Sealed<T extends { new (...args: any[]): {} }>(value: T) {
  Object.seal(value);
  Object.seal(value.prototype);
}

표준에서도 클래스 데코레이터는 “클래스 값 자체”를 받아 수정/교체할 수 있습니다. 다만 legacy와 달리 반환 규칙이 더 명확해지므로, 반환하지 않을 거면 확실히 side-effect만 두는 식으로 작성하세요.

6) 라이브러리 호환성: NestJS/TypeORM 사용 시 체크리스트

NestJS/TypeORM 조합에서 TS 5.6 데코레이터 오류가 나면, 대개 “표준 전환”이 아니라 legacy 고정이 빠른 해결책입니다. 아래를 체크하세요.

  1. experimentalDecorators: true가 꺼져 있지 않은가
  2. emitDecoratorMetadata: true가 빠지지 않았는가
  3. reflect-metadata가 엔트리포인트에서 1회 import 되는가
// main.ts 혹은 entry
import "reflect-metadata";
  1. 테스트 환경(Jest)에서 tsconfig를 다른 걸 쓰지 않는가
  2. SWC/Babel을 쓰면 데코레이터 변환 옵션이 TS 설정과 동일한가

특히 monorepo에서 앱/패키지별로 tsconfig가 달라 “어떤 곳은 legacy, 어떤 곳은 표준”이 되어버리면, 컴파일 오류가 랜덤처럼 보입니다.

7) 마이그레이션 전략: 한 번에 갈아엎지 않는 방법

표준 데코레이터로 가고 싶더라도, 한 번에 전체를 바꾸면 리스크가 큽니다. 현실적인 순서는 다음입니다.

  1. 프로젝트를 legacy로 고정해 빌드를 안정화(배포/CI 복구)
  2. 데코레이터를 사용하는 코드(사내 데코레이터)만 별도 패키지로 분리
  3. 분리된 패키지부터 표준 데코레이터로 전환 + 소비자 코드 점진 적용

이 방식은 “업그레이드로 CI가 깨지는 상황”을 빠르게 수습하면서도, 장기적으로 표준으로 이동할 통로를 확보합니다.

버전 업으로 타입이 엄격해져서 함께 터지는 오류(예: 인덱스 접근)까지 동시에 정리해야 한다면, 앞서 언급한 TypeScript 5.5+ noUncheckedIndexedAccess 오류 실전해결을 같이 적용하면 마이그레이션 비용을 줄일 수 있습니다.

8) 디버깅 팁: “컴파일러가 어떤 데코레이터로 해석했는지” 확인

데코레이터 문제는 결국 “어떤 트랜스폼이 적용됐는가”입니다.

  • tsc --showConfig로 최종 적용된 옵션을 확인
  • 빌드 산출물(JS)에서 데코레이터 헬퍼가 어떤 형태로 들어갔는지 확인
  • Jest/ts-jest가 별도의 tsconfig를 쓰는지 확인

간단한 확인용으로, 데코레이터 함수에 들어오는 인자를 로그로 찍어보면(개발 환경에서만) legacy인지 표준인지 바로 드러납니다.

export function DebugDecorator(...args: any[]) {
  console.log("decorator args len:", args.length, args);
}

class A {
  @DebugDecorator
  method() {}
}
  • legacy 메서드 데코레이터면 보통 3개(target, key, descriptor)
  • 표준 메서드 데코레이터면 2개(value, context)

9) 정리

TS 5.6 데코레이터 컴파일 오류는 “문법이 깨졌다”라기보다, 프로젝트가 기대하는 데코레이터 모델(legacy vs 표준)과 실제 컴파일/트랜스폼 모델이 어긋난 결과인 경우가 대부분입니다.

  • NestJS/TypeORM/reflect-metadata 중심이면: legacy 고정이 안전
  • 자체 데코레이터 중심이고 장기 유지보수 관점이면: 표준 데코레이터로 점진 전환
  • 무엇보다 tsconfig, 테스트, 번들러까지 일관된 모델로 맞추는 게 핵심

업그레이드 과정에서 ESM 전환까지 겹쳐 빌드가 더 흔들린다면, Node 22에서 require가 안 될 때 ESM 전환법도 함께 점검해 데코레이터/모듈 시스템 이슈를 분리해서 해결하는 것을 권장합니다.