- Published on
TypeScript 5.x 데코레이터 TS1240 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/프론트에서 데코레이터를 쓰다 TypeScript 5.x로 올린 뒤 갑자기 TS1240이 터지는 경우가 많습니다. 특히 class-validator, typeorm, nestjs 같은 생태계는 한동안 레거시(legacy) 데코레이터 전제 코드가 많았고, TypeScript 5.x부터는 표준(ECMAScript) 데코레이터 지원이 본격화되면서 타입 시스템 관점에서 “데코레이터 함수가 기대하는 인자 형태”가 달라졌기 때문입니다.
이 글에서는 TS1240이 왜 생기는지, 그리고 프로젝트 상황별로 어떤 선택지가 현실적인지(레거시 유지 vs 표준 전환 vs 혼합 대응)를 정리합니다.
TS1240이 의미하는 것: 데코레이터 시그니처 불일치
TS1240은 요약하면 데코레이터를 적용할 수 없는 위치이거나, 더 흔하게는 데코레이터 함수 타입(시그니처)이 TypeScript가 기대하는 형태와 맞지 않다는 뜻입니다.
TypeScript 5.x에서 문제가 커진 이유는 다음 두 축이 겹치기 때문입니다.
- 레거시 데코레이터(실험적): 예전 방식.
target,propertyKey,descriptor같은 인자를 받는 형태 - 표준 데코레이터(ECMAScript proposal 기반): 새로운 방식.
value와context를 받는 형태(예:ClassMethodDecoratorContext등)
즉, 프로젝트는 레거시 스타일 데코레이터를 쓰고 있는데 컴파일러가 표준 데코레이터 시그니처로 해석하거나(또는 그 반대) 하면 TS1240이 발생합니다.
가장 흔한 재현 예: 레거시 방식 메서드 데코레이터
다음은 레거시 스타일의 메서드 데코레이터입니다.
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);
};
}
class Service {
@Log
run(x: number) {
return x * 2;
}
}
TypeScript가 위 코드를 표준 데코레이터 모드로 해석하면, @Log는 (value, context)를 받는 함수여야 하는데 (target, key, descriptor)를 받으니 타입 불일치가 발생하며 TS1240이 나옵니다.
1) 빠른 해결: 레거시 데코레이터로 고정하기(대부분의 실무 프로젝트)
현재 생태계(특히 ORM/검증/DI 계열)에서 가장 안전한 단기 처방은 레거시 데코레이터를 계속 쓰도록 설정을 명확히 하는 것입니다.
tsconfig에서 확인할 설정
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
experimentalDecorators: 레거시 데코레이터 사용에 핵심emitDecoratorMetadata: NestJS/TypeORM/class-validator 등에서 런타임 메타데이터에 의존할 때 필요
체크포인트
- 모노레포라면 패키지별
tsconfig가 서로 다른 설정을 갖고 일부만 깨지는 경우가 흔합니다. ts-jest,ts-node,swc,esbuild등을 섞어 쓰면 “실제 빌드에 사용되는 tsconfig”가 달라서 동일 코드가 환경별로 다르게 깨질 수 있습니다.
빌드가 CI에서만 깨진다면 캐시/빌드 설정이 꼬였을 가능성도 큽니다. CI 쪽 원인 추적은 GitHub Actions 캐시가 안 먹을 때 key 전략과 디버깅도 함께 참고하면 좋습니다.
2) 근본 원인 분리: 지금 내 프로젝트가 레거시인지 표준인지 확인
TypeScript 5.x에서는 “데코레이터를 어떤 의미로 해석하는가”가 핵심입니다. 아래처럼 간단히 확인할 수 있습니다.
표준 데코레이터 스타일 함수가 통과하는지 테스트
function StdMethodDecorator(
value: Function,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
console.log("std", String(context.name));
return value.apply(this, args);
};
}
class A {
@StdMethodDecorator
m() {
return 1;
}
}
- 위 코드가 통과하고 레거시가 깨진다면, 컴파일러/트랜스파일러 파이프라인이 표준 쪽으로 기울어졌을 가능성이 큽니다.
- 반대로 위 코드가 깨지고 레거시가 통과하면 레거시 모드에 가깝습니다.
실무에서는 “tsc는 레거시로 보는데, Babel/SWC가 표준으로 처리” 같은 혼종 상태도 나옵니다. 이때 TS1240은 타입 단계에서 터지기도 하고, 런타임에서 데코레이터 동작이 이상해지기도 합니다.
3) 표준 데코레이터로 전환하는 방법(장기 전략)
새 프로젝트이거나 데코레이터를 직접 작성해 쓰는 비중이 높다면, 장기적으로는 표준 데코레이터로 옮기는 편이 좋습니다. 다만 다음 제약이 있습니다.
- 레거시의
emitDecoratorMetadata기반 생태계를 그대로 가져가기 어렵습니다. - 프레임워크/라이브러리가 표준 데코레이터를 공식 지원해야 합니다.
표준 필드 데코레이터 예시
표준 데코레이터에서는 필드/메서드/접근자마다 context 타입이 다르고, 초기화 훅을 제공하는 등 모델이 다릅니다.
function Default(value: unknown) {
return function (
_initialValue: unknown,
context: ClassFieldDecoratorContext
) {
context.addInitializer(function () {
const key = context.name;
if (typeof key === "string") {
(this as any)[key] = (this as any)[key] ?? value;
}
});
};
}
class User {
@Default("guest")
role!: string;
}
레거시의 target/propertyKey 감각으로 작성하면 바로 TS1240류 문제가 생기므로, 전환 시에는 “어떤 요소에 어떤 context가 오는지”를 기준으로 다시 설계해야 합니다.
4) 라이브러리 데코레이터에서 TS1240이 날 때: 내 코드가 아니라 의존성 문제
TS1240이 내 데코레이터가 아니라 node_modules 타입에서 발생한다면 대개 다음 중 하나입니다.
- 라이브러리가 레거시 데코레이터 타입을 가정하는데, 프로젝트가 표준 모드로 해석
- 반대로 라이브러리가 표준 타입 정의를 제공하는데, 프로젝트가 레거시 기반
skipLibCheck가 꺼져 있어 타입 충돌이 표면화
응급 처치: skipLibCheck
단기적으로 빌드를 살려야 한다면 다음 설정이 도움이 될 수 있습니다.
{
"compilerOptions": {
"skipLibCheck": true
}
}
다만 이건 “증상을 숨기는” 성격이라, 장기적으로는 데코레이터 모드 정리(레거시 고정 또는 표준 전환)와 의존성 업데이트가 필요합니다.
5) TS1240을 안정적으로 없애는 실전 체크리스트
1) 데코레이터 파이프라인을 하나로 통일
tsc로 빌드하는지- Next.js/ts-jest/SWC/Babel이 데코레이터를 어떻게 처리하는지
- 개발 서버와 프로덕션 빌드의 설정이 동일한지
특히 CI에서만 깨지는 경우는 캐시/빌드 산출물 불일치가 흔합니다. 빌드 속도 때문에 캐시를 적극적으로 쓰는 팀이라면 GitHub Actions 캐시 무효화로 빌드가 느릴 때도 같이 점검해보세요.
2) tsconfig를 패키지 단위로 명시하고 상속 구조를 단순화
모노레포에서 tsconfig.base.json을 두고 패키지별로 상속할 때, 어떤 패키지에선 experimentalDecorators가 빠져 TS1240이 터지는 패턴이 많습니다.
3) 레거시 데코레이터를 쓰는 프레임워크라면 레거시를 선택
NestJS/TypeORM/class-validator 조합은 여전히 레거시 기반 메타데이터 의존이 강합니다. 이 경우 표준 데코레이터로 무리하게 전환하면 TS1240은 잡아도 런타임 기능이 무너질 수 있습니다.
4) 직접 만든 데코레이터는 “어느 모드용인지” 주석과 테스트를 추가
데코레이터는 컴파일러 옵션에 민감하므로, 최소한 아래를 권장합니다.
- 데코레이터 유닛 테스트 1개
tsconfig변경 시 깨지는지 확인하는 타입 테스트(예:tsd)
6) 레거시/표준 데코레이터를 동시에 지원해야 한다면
라이브러리를 만들거나, 여러 소비자 프로젝트를 지원해야 한다면 “두 시그니처를 모두 받는” 오버로드 전략을 고려할 수 있습니다.
아래는 개념 예시입니다.
// 레거시 시그니처
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): void;
// 표준 시그니처
function Log(
value: Function,
context: ClassMethodDecoratorContext
): Function | void;
// 구현
function Log(...args: any[]) {
// 표준: (value, context)
if (args.length === 2 && typeof args[1] === "object") {
const value = args[0] as Function;
const context = args[1] as ClassMethodDecoratorContext;
return function (this: any, ...fnArgs: any[]) {
console.log("std", String(context.name));
return value.apply(this, fnArgs);
};
}
// 레거시: (target, key, descriptor)
const descriptor = args[2] as PropertyDescriptor;
const propertyKey = args[1] as string;
const original = descriptor.value;
descriptor.value = function (...fnArgs: any[]) {
console.log("legacy", propertyKey);
return original.apply(this, fnArgs);
};
}
이 방식은 “타입 에러를 줄이는” 데는 도움이 되지만, 실제로는 런타임 변환기(Babel/SWC/tsc)가 어느 규약으로 데코레이터를 호출하는지에 따라 결과가 달라집니다. 즉, 혼합 지원은 최후의 수단이고, 가능하면 프로젝트별로 하나의 규약을 확정하는 편이 안전합니다.
마무리: TS1240은 설정 문제가 아니라 ‘규약 충돌’이다
TS1240은 단순히 타입스크립트 버그가 아니라, 레거시 데코레이터와 표준 데코레이터라는 서로 다른 규약이 충돌하면서 생기는 대표적인 증상입니다.
- 기존 프레임워크 기반 프로젝트:
experimentalDecorators와emitDecoratorMetadata를 중심으로 레거시 모드를 고정 - 신규/경량 프로젝트: 표준 데코레이터로 전환하되, 메타데이터 의존 설계를 재검토
- CI에서만 실패: 캐시/빌드 설정과 실제 적용되는
tsconfig를 우선 확인
데코레이터는 편리하지만 빌드 파이프라인의 영향을 크게 받습니다. 한 번 TS1240을 잡을 때 “어떤 규약을 쓰는지”를 문서화해두면, 다음 TypeScript 메이저 업데이트에서도 같은 종류의 장애를 훨씬 빠르게 끝낼 수 있습니다.