- Published on
TypeScript 5.6 데코레이터 마이그레이션 에러 총정리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 라이브러리(예: NestJS, TypeORM, class-validator, inversify 등)를 쓰는 프로젝트에서 TypeScript 5.6 업그레이드는 생각보다 자주 “데코레이터 지옥”으로 이어집니다. 이유는 간단합니다. TypeScript가 오랫동안 지원해온 레거시(실험적) 데코레이터와, ECMAScript 표준 데코레이터가 의미적으로 다르기 때문입니다.
TypeScript 5.6은 표준 데코레이터 지원이 더 성숙해졌고, 생태계도 천천히 그 방향으로 이동 중입니다. 하지만 현실의 코드베이스는 레거시 데코레이터 기반 메타데이터(reflect-metadata)와 프레임워크 런타임에 강하게 결합돼 있어, 설정 하나 바꿨다가 컴파일 에러뿐 아니라 런타임에서 조용히 깨지는 경우가 많습니다.
이 글은 “TypeScript 5.6 데코레이터 마이그레이션”에서 실제로 자주 만나는 에러/증상을 원인별로 분류하고, 어떤 선택지가 있는지(레거시 유지 vs 표준 전환), 그리고 전환한다면 어떤 코드/설정 변경이 필요한지까지 예제로 정리합니다.
참고: 프런트엔드에서 빌드/런타임 불일치 문제를 함께 겪는 경우도 많습니다. Next.js 환경이라면 Next.js Hydration Mismatch 5가지 원인과 해결법도 같이 점검해보세요.
1) 먼저 정리: 레거시 데코레이터 vs 표준 데코레이터
레거시(실험적) 데코레이터
tsconfig.json에서experimentalDecorators: true로 활성화emitDecoratorMetadata: true를 켜면design:type같은 메타데이터를reflect-metadata에 기록- 많은 프레임워크/라이브러리가 이 동작에 의존
표준(ECMAScript) 데코레이터
- 데코레이터 시그니처/평가 방식이 다름
- 메타데이터 자동 방출(
emitDecoratorMetadata) 같은 레거시 편의 기능과 결이 다름 - 기존 레거시 데코레이터 코드를 그대로 두고 설정만 바꾸면 깨질 확률이 큼
결론적으로 “TypeScript 5.6으로 올렸더니 데코레이터가 깨졌다”는 대부분 버그가 아니라 모드 불일치입니다.
2) 가장 흔한 원인: tsconfig 설정 충돌/오해
증상 A: 데코레이터 관련 타입 에러가 폭발
대표적으로 다음과 같은 류의 에러가 나옵니다.
- 데코레이터 시그니처가 맞지 않는다
- 데코레이터 반환 타입이 기대와 다르다
- 파라미터 데코레이터가 허용되지 않는다
이때 가장 먼저 확인할 것은 “현재 빌드가 레거시 데코레이터로 돌고 있다고 믿었는데, 실제로는 표준 데코레이터로 해석되고 있는지”입니다.
체크리스트
tsconfig.json에experimentalDecorators가 켜져 있는가tsconfig가 여러 개라면(모노레포, Next.js, ts-jest 등) 실제로 어떤 설정이 적용되는가- 빌드 체인이 TypeScript가 아니라 Babel/SWC라면 데코레이터 처리 방식이 다른가
예시: 레거시 데코레이터 유지(안정 우선)
레거시 기반 프레임워크를 쓰는 서버 프로젝트라면, 당장 표준으로 갈 이유가 없다면 아래처럼 “레거시 고정”이 가장 안전합니다.
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
- 핵심은
experimentalDecorators와emitDecoratorMetadata조합입니다. reflect-metadata를 쓰는 라이브러리는 보통 이 조합이 필요합니다.
증상 B: 컴파일은 되는데 런타임에서 DI/Validation이 조용히 실패
예를 들어 NestJS에서 DI가 안 되거나, class-transformer/class-validator가 타입 정보를 못 읽어서 validation이 통과하거나 실패하는 식으로 “이상하게 동작”하는 경우가 있습니다.
원인
emitDecoratorMetadata가 꺼졌거나reflect-metadata가 로드되지 않았거나- 번들러가 데코레이터/메타데이터 방출을 기대와 다르게 처리했거나
해결 패턴
엔트리포인트 최상단에서 reflect-metadata를 반드시 로드합니다.
import "reflect-metadata";
import { bootstrap } from "./bootstrap";
bootstrap();
그리고 tsconfig에서 메타데이터 방출을 확인합니다.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
3) 표준 데코레이터로 전환할 때 터지는 대표 에러 카테고리
표준 데코레이터로 가는 이유는 보통 다음 중 하나입니다.
- 장기적으로 표준을 따르고 싶다
- 라이브러리가 표준 데코레이터를 지원하기 시작했다
- 번들러/런타임 환경에서 표준 데코레이터가 더 자연스럽다
하지만 전환 시에는 “기존 레거시 데코레이터 함수”가 그대로는 맞지 않습니다.
3.1 클래스/메서드/필드 데코레이터 시그니처 불일치
레거시 데코레이터는 대체로 다음처럼 target, propertyKey, descriptor 형태를 받습니다.
function LegacyMethodDec(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// ...
}
표준 데코레이터는 value 와 context를 받는 형태로 바뀝니다.
function StdMethodDec(value: Function, context: ClassMethodDecoratorContext) {
// ...
}
마이그레이션 포인트
propertyKey를 직접 받지 않습니다. 대신context.name을 봅니다.descriptor를 직접 만지기보다,value(원 함수)를 래핑해 반환하는 식이 일반적입니다.
예: 메서드 실행 시간을 로깅하는 데코레이터
export function LogTime() {
return function (value: Function, context: ClassMethodDecoratorContext) {
const name = String(context.name);
return function (this: any, ...args: any[]) {
const start = performance.now();
try {
return value.apply(this, args);
} finally {
const end = performance.now();
console.log(`[${name}] ${(end - start).toFixed(2)}ms`);
}
};
};
}
3.2 파라미터 데코레이터 의존 코드가 막히는 문제
레거시 생태계는 파라미터 데코레이터(예: @Inject() 같은)를 많이 씁니다. 표준 데코레이터로 전환하면 여기서 막히는 경우가 많습니다.
증상
- “파라미터 데코레이터가 지원되지 않는다” 류의 컴파일 에러
- 프레임워크가 파라미터 메타데이터를 수집하지 못해 런타임 실패
대응 전략
- 프레임워크/라이브러리가 표준 데코레이터를 완전히 지원하는지 확인
- 지원하지 않는다면 레거시 모드 유지가 현실적인 선택
- 또는 파라미터 데코레이터 기반 설계를 “명시적 팩토리/등록 코드”로 변경
예: DI를 파라미터 데코레이터 대신 명시적 주입으로 바꾸는 패턴(개념 예시)
class UserService {
constructor(private readonly repo: UserRepo) {}
}
const userService = new UserService(container.resolve(UserRepo));
4) emitDecoratorMetadata 관련 오해: 표준 전환 시 자동 메타데이터가 사라질 수 있음
레거시에서는 emitDecoratorMetadata를 켜면 타입 정보가 런타임에 남는 것처럼 보였습니다. 많은 라이브러리가 이 메타데이터를 읽어 동작합니다.
표준 데코레이터로 전환하면 다음 문제가 생깁니다.
- 동일한 방식으로 타입 메타데이터가 제공되지 않거나
- 제공되더라도 라이브러리가 기대하는 키/형태가 달라져서
- 결과적으로 validation/serialization/DI가 깨집니다.
증상
- class-validator에서 DTO validation이 이상해짐
- class-transformer가 타입을 추론 못 함
- ORM이 컬럼 타입을 추론 못 함
해결 방향
- 라이브러리가 표준 데코레이터 + 메타데이터 전략을 공식 지원하는지 확인
- 지원 전이라면 레거시 유지
- 전환을 강행해야 한다면, 타입 정보를 “명시적 설정”으로 바꾸는 작업이 필요
예: 타입 추론 대신 명시적 타입 선언을 늘리는 방향(개념 예시)
class User {
// 예: 데코레이터가 타입을 못 읽는다면
// 컬럼 타입을 옵션으로 명시하는 식으로 우회
// (실제 ORM 옵션은 라이브러리마다 다름)
id!: number;
name!: string;
}
5) Babel/SWC/ts-jest 등 툴체인에서 생기는 “설정은 맞는데 빌드가 깨짐”
TypeScript 컴파일러(tsc)로만 빌드하면 비교적 예측 가능하지만, 실제 프로젝트는 다음이 섞입니다.
- Next.js의 SWC
- Babel 플러그인
- Jest의 트랜스폼(ts-jest 또는 babel-jest)
- ts-node
이때 “tsconfig는 레거시인데, 테스트만 표준으로 해석” 같은 분열이 생깁니다.
체크 포인트
- Jest가 참조하는
tsconfig가 무엇인지 - Next.js가 사용하는 트랜스폼 설정이 무엇인지
- 서버 코드와 프런트 코드의 tsconfig가 분리되어 있는지
예: Jest에서 tsconfig를 명시
// jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
globals: {
"ts-jest": {
tsconfig: "tsconfig.json"
}
}
};
6) 자주 나오는 에러/증상별 빠른 처방전
6.1 “데코레이터 시그니처가 맞지 않음” 타입 에러
- 원인: 레거시 데코레이터 함수를 표준 모드에서 사용
- 처방: 레거시 유지면
experimentalDecorators: true로 고정, 표준 전환이면 데코레이터 구현을value,context기반으로 재작성
6.2 DI가 갑자기 안 되고 undefined 주입
- 원인:
emitDecoratorMetadata꺼짐 또는reflect-metadata미로드 - 처방: 엔트리에서
reflect-metadataimport,emitDecoratorMetadata: true
6.3 테스트에서만 깨짐
- 원인: Jest 트랜스폼이 다른 tsconfig를 사용하거나 Babel 설정이 다름
- 처방: Jest 설정에서 tsconfig 고정, 트랜스폼 체인 단순화
6.4 번들러 적용 후 런타임만 깨짐
- 원인: SWC/Babel이 데코레이터를 다르게 변환, 또는 메타데이터가 트리쉐이킹/최적화로 누락
- 처방: 번들러의 데코레이터 옵션 확인, 메타데이터 보존 전략 점검
7) 마이그레이션 전략: “전면 전환”보다 “경계면부터”
대부분의 팀에게 현실적인 전략은 다음 중 하나입니다.
전략 1: 레거시 데코레이터 유지 + TS 5.6만 사용
- 서버 프레임워크/ORM/validation이 레거시 데코레이터에 의존한다면 최우선 추천
- 업그레이드 이점(타입 시스템/성능/언어 기능)을 누리면서 위험 최소화
전략 2: 표준 데코레이터는 신규 코드부터 적용
- 신규 패키지/모듈에서만 표준 데코레이터를 쓰고
- 기존 레거시 영역은 그대로 둔 채 경계면을 점진적으로 이동
전략 3: 라이브러리 교체/업그레이드와 함께 전환
- 표준 데코레이터 지원이 확실한 버전으로 올린 후 전환
- 이때 “메타데이터 의존”을 줄이는 리팩터링이 병행돼야 합니다.
8) 디버깅 팁: 에러를 ‘데코레이터 모드’ 문제로 빠르게 확정하는 법
- 문제 재현 최소화
- 데코레이터 하나만 남긴 최소 예제로
tsc컴파일
- 결과 JS를 직접 확인
- 데코레이터가 어떤 헬퍼로 변환되는지 보고 레거시/표준 여부를 추정
- 실행 시점 검증
reflect-metadata가 로드되었는지Reflect.getMetadata가 기대대로 값을 내는지
예: 메타데이터 존재 여부 확인(레거시 기반)
import "reflect-metadata";
class A {
x!: number;
}
const t = Reflect.getMetadata("design:type", A.prototype, "x");
console.log(t);
여기서 undefined가 나오면, 대개 emitDecoratorMetadata 미설정 또는 변환 단계에서 메타데이터 방출이 누락된 것입니다.
9) 마무리
TypeScript 5.6에서 데코레이터 마이그레이션이 어려운 이유는 “문법이 조금 바뀌었다” 수준이 아니라, 레거시와 표준이 서로 다른 모델이기 때문입니다. 따라서 에러를 개별적으로 땜질하기보다, 먼저 프로젝트가 어떤 생태계(레거시 메타데이터 의존형인지, 표준 데코레이터로 갈 준비가 됐는지)를 명확히 한 뒤 전략을 택해야 합니다.
- NestJS/TypeORM/class-validator 중심이면: 레거시 유지가 대체로 정답
- 신규 라이브러리/경량 런타임 중심이면: 표준 데코레이터로 점진 전환
빌드 파이프라인이 복잡한 팀일수록 “설정은 맞는데 특정 환경에서만 깨지는” 일이 잦습니다. 이런 류의 환경 의존 문제를 다루는 흐름이 익숙하다면, 네트워크/플랫폼 이슈를 원인별로 쪼개 해결하는 방식이 도움이 됩니다. 예를 들어 OpenAI Responses API 400 invalid_request_error 해결 가이드처럼, 증상을 분류하고 재현 조건을 좁혀가는 접근이 데코레이터 디버깅에도 그대로 통합니다.