- Published on
TS 5.6 Decorators에서 Unable to resolve signature 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 인프라 오류보다 더 난감한 순간이 있습니다. 빌드는 되던 코드가 TypeScript 업그레이드 후 갑자기 타입 에러를 뿜고, 메시지는 모호한데 원인은 여러 갈래로 갈라지는 경우입니다.
TS 5.6에서 데코레이터(Decorators)를 적용하거나, 기존 레거시 데코레이터 코드를 유지한 채 설정을 건드리다 보면 대표적으로 아래 형태의 오류를 만나기 쉽습니다.
Unable to resolve signature of method decorator when called as an expressionUnable to resolve signature of property decorator when called as an expression
이 글은 “왜 이런 메시지가 뜨는지”를 먼저 구조적으로 설명하고, 실제로 프로젝트에서 가장 많이 걸리는 케이스(설정 충돌, 데코레이터 시그니처 불일치, 라이브러리 버전 미스매치)를 빠르게 분리 진단하는 체크리스트와 수정 예시를 제공합니다.
관련해서 TS 버전 업그레이드로 타입 체킹이 강화되며 에러가 늘어나는 패턴은 다른 옵션에서도 반복됩니다. 예를 들어 noUncheckedIndexedAccess 도 비슷한 성격의 “기존에는 묵인되던 타입 구멍이 노출되는” 케이스가 많습니다. 필요하면 TS 5.5 noUncheckedIndexedAccess 에러 해결 가이드도 같이 참고하면 좋습니다.
TS 5.6 데코레이터: 레거시 vs 새 표준의 핵심 차이
TypeScript에서 데코레이터는 크게 두 계열로 나뉩니다.
- 레거시(기존) 데코레이터:
experimentalDecorators기반 - 새 표준 데코레이터: ECMAScript Decorators 스펙을 따르는 형태(컨텍스트 객체 등)
문제는 “코드/라이브러리는 레거시 방식으로 작성되어 있는데, 컴파일러는 새 데코레이터 시그니처로 해석하려고 하거나(또는 반대)” 같은 어긋남이 생기면, 타입 시스템이 데코레이터 호출 시그니처를 맞추지 못해 Unable to resolve signature 류의 메시지를 내기 쉽다는 점입니다.
즉 이 에러는 대개 다음 중 하나입니다.
- tsconfig 설정이 레거시/표준 중 무엇을 쓰는지와 코드가 불일치
- 데코레이터 팩토리(함수를 반환하는 데코레이터)의 반환 타입/파라미터 타입이 애매하거나 잘못됨
- 라이브러리(예: ORM/DI/Validation)가 기대하는 데코레이터 모델과 TS 설정이 불일치
가장 먼저 할 일: tsconfig에서 데코레이터 모드 확정하기
프로젝트가 사용하는 데코레이터 생태계를 먼저 확정해야 합니다.
- NestJS, TypeORM 일부 버전,
class-validator,class-transformer등은 여전히 레거시 데코레이터 전제를 많이 깔고 있습니다. - 반대로 최신 표준 데코레이터(컨텍스트 기반)를 직접 쓰려면, 코드도 그 시그니처에 맞춰야 합니다.
레거시 데코레이터를 쓸 때 권장 설정
레거시 생태계를 쓴다면 보통 아래 조합이 필요합니다.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
experimentalDecorators: 레거시 데코레이터 활성화emitDecoratorMetadata: 런타임 메타데이터가 필요한 프레임워크에서 사용
이 조합이 빠지면, 타입 에러뿐 아니라 런타임에서 DI/Validation이 깨지는 경우도 많습니다.
새 표준 데코레이터를 쓸 때의 관점
새 표준 데코레이터는 “타깃과 키” 대신 “값과 컨텍스트”를 받는 형태가 중심입니다.
예를 들어 메서드 데코레이터는 대략 이런 모양입니다.
function Log(
value: Function,
context: ClassMethodDecoratorContext
) {
return function (this: unknown, ...args: unknown[]) {
console.log(String(context.name), args)
return value.apply(this, args)
}
}
레거시 메서드 데코레이터가 (target, propertyKey, descriptor) 를 받는 것과 완전히 다릅니다. 따라서 “레거시 시그니처로 작성한 데코레이터”를 “표준 데코레이터 모드”에서 돌리면, 타입 시스템이 맞출 수 없어서 Unable to resolve signature 로 터질 가능성이 매우 큽니다.
증상으로 원인 빠르게 분류하기
Unable to resolve signature 는 메시지가 비슷해도 원인이 다릅니다. 아래처럼 분류하면 빠릅니다.
1) @Decorator() 형태에서만 터진다: 데코레이터 팩토리 반환 타입 문제
데코레이터를 “호출해서” 쓰는 경우, 즉 @Log() 같은 형태는 데코레이터 팩토리입니다.
이때 가장 흔한 실수는 “팩토리가 데코레이터 함수를 반환하지 않거나”, “반환 타입이 너무 넓어서(예: Function) 컴파일러가 시그니처를 확정 못 하는” 경우입니다.
잘못된 예
function Log() {
// 반환 타입이 불명확하거나, 데코레이터 시그니처가 아예 다름
return (x: any) => x
}
class A {
@Log()
m() {}
}
위 코드는 m() 위치에서 “메서드 데코레이터가 와야 하는데, 실제 반환된 함수가 그 시그니처가 아니다”라는 문제가 됩니다.
레거시 메서드 데코레이터로 고치기
function Log(): MethodDecorator {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const original = descriptor.value
descriptor.value = function (...args: unknown[]) {
console.log(String(propertyKey), args)
return original.apply(this, args)
}
return descriptor
}
}
class A {
@Log()
m(x: number) {
return x + 1
}
}
핵심은 “반환 타입을 MethodDecorator 로 고정”해서, 컴파일러가 해당 위치에서 요구하는 데코레이터 시그니처와 매칭할 수 있게 만드는 것입니다.
2) 프로퍼티 데코레이터에서 터진다: 표준 데코레이터에서는 모델이 다름
레거시 프로퍼티 데코레이터는 보통 (target, propertyKey) 형태인데, 표준 데코레이터의 필드 데코레이터는 컨텍스트 기반입니다.
레거시 전제로 작성된 라이브러리 데코레이터를 표준 모드로 해석하려고 하면, 특히 필드에서 자주 깨집니다.
레거시 프로퍼티 데코레이터 예시
function Required(): PropertyDecorator {
return (target: Object, propertyKey: string | symbol) => {
// 메타데이터 저장 로직 등
}
}
class User {
@Required()
name!: string
}
이 코드는 레거시 모드에서는 정상인데, 표준 데코레이터로 해석되면 시그니처가 맞지 않아 에러가 날 수 있습니다.
해결은 둘 중 하나입니다.
- 프로젝트를 레거시 데코레이터 모드로 고정(대부분의 기존 프레임워크는 이쪽)
- 표준 데코레이터 시그니처로 데코레이터 구현을 바꾸고, 해당 생태계에 맞는 라이브러리로 교체
3) 특정 라이브러리 데코레이터에서만 터진다: 버전 미스매치 가능성
예를 들어 NestJS/TypeORM/class-validator 조합에서 TS만 올리거나, 반대로 라이브러리만 올렸을 때 “라이브러리의 타입 선언이 기대하는 데코레이터 타입”과 “현재 TS가 활성화한 데코레이터 모델”이 충돌할 수 있습니다.
이 경우 해결 전략은 다음 순서가 안전합니다.
- tsconfig에서 레거시/표준을 먼저 확정
- 해당 모드에 맞는 라이브러리 버전 조합을 맞춤
- 데코레이터를 직접 구현한 코드가 있다면 반환 타입을
ClassDecorator,MethodDecorator,PropertyDecorator,ParameterDecorator로 명시
실전 체크리스트: Unable to resolve signature 를 끊는 6단계
1) 에러가 나는 데코레이터가 “팩토리인지” 확인
@X는 데코레이터 함수@X()는 데코레이터 팩토리
팩토리라면 “반환 타입이 정확한 데코레이터 타입인지”를 먼저 의심합니다.
2) 반환 타입을 반드시 표준 타입으로 고정
레거시라면 아래처럼 명시합니다.
export function D(): MethodDecorator {
return () => {
// ...
}
}
이 한 줄이 Unable to resolve signature 류의 오류를 가장 많이 줄여줍니다.
3) 오버로드/제네릭 데코레이터는 시그니처를 분리
데코레이터가 오버로드처럼 동작하면, 컴파일러가 “어떤 호출 시그니처인지” 확정 못 하는 경우가 있습니다.
이때는 오버로드를 명시하고 구현부에서는 넓게 받되, 반환은 정확히 데코레이터 타입으로 고정합니다.
export function Cache(ttlMs: number): MethodDecorator
export function Cache(): MethodDecorator
export function Cache(ttlMs?: number): MethodDecorator {
return (_t, key, desc) => {
const original = desc.value
desc.value = function (...args: unknown[]) {
// ttlMs 사용
return original.apply(this, args)
}
return desc
}
}
4) emitDecoratorMetadata 가 필요한지 점검
DI나 validation에서 런타임 타입 정보가 필요하면 켜야 합니다.
다만 이것은 Unable to resolve signature 의 직접 원인이라기보다, “설정을 고치다 데코레이터 관련 옵션을 바꾸는 과정에서” 같이 틀어져서 문제가 커지는 경우가 많습니다.
5) useDefineForClassFields 와 필드 초기화 타이밍 이슈 확인
프로퍼티 데코레이터를 쓰는 라이브러리는 필드 초기화 타이밍에 민감할 수 있습니다. 타입 에러는 아니더라도, TS 업그레이드 후 데코레이터 동작이 달라졌다고 느끼면 이 옵션과 target 조합을 같이 점검해야 합니다.
6) 최소 재현 코드로 “내 코드 문제 vs 라이브러리 문제” 분리
에러가 특정 라이브러리 데코레이터에서만 난다면, 최소 재현을 만들어 분리하는 게 가장 빠릅니다.
- 데코레이터를 하나 직접 만들어
MethodDecorator를 반환하게 했을 때는 정상인가 - 라이브러리 데코레이터만 붙이면 깨지는가
여기서 후자라면 버전/설정 호환성 문제일 확률이 큽니다.
예제: 흔한 실패 케이스와 올바른 패턴
실패 케이스: 반환 타입이 any 로 퍼져 시그니처 추론 실패
const Measure = () => {
return (target: any, key: any, desc: any) => {
const original = desc.value
desc.value = function (...args: any[]) {
const t0 = Date.now()
const r = original.apply(this, args)
console.log(Date.now() - t0)
return r
}
}
}
이 코드는 프로젝트 설정/TS 버전에 따라 “우연히 통과”하기도 하지만, TS가 엄격해지면 데코레이터 위치에서 기대하는 시그니처와 어긋나면서 에러가 날 수 있습니다.
수정 케이스: 타입을 고정하고 PropertyDescriptor 를 반환
export function Measure(): MethodDecorator {
return (_target, key, descriptor) => {
const original = descriptor.value as (...args: unknown[]) => unknown
descriptor.value = function (...args: unknown[]) {
const t0 = Date.now()
const r = original.apply(this, args)
console.log(String(key), Date.now() - t0)
return r
}
return descriptor
}
}
포인트는 아래 2개입니다.
- 팩토리 반환 타입을
MethodDecorator로 고정 descriptor.value를 함수로 단언해도 되지만, 최소한unknown기반으로 안전하게 유지
TS 5.6에서 특히 자주 꼬이는 조합
isolatedModules 또는 빌드 체인 변경과 함께 발생
Next.js, Vite, SWC, Babel 등과 조합되면 “타입 체크는 TS가 하는데, 실제 트랜스파일은 다른 도구가 수행”하는 구조가 됩니다. 이때 데코레이터 트랜스폼 지원 여부가 다르면, 타입 에러를 고쳐도 런타임이 깨질 수 있습니다.
- 타입 에러(
Unable to resolve signature)는 TS 타입 레벨 문제 - 런타임 데코레이터 동작은 트랜스파일러 지원 문제
따라서 “타입만 맞추면 끝”이라고 보기 어렵고, 빌드 체인에서 데코레이터를 어떻게 처리하는지까지 확인해야 합니다.
Next.js 환경에서 빌드/렌더 파이프라인 문제를 자주 겪는다면 Next.js Hydration mismatch 원인 7가지와 해결법처럼, 프레임워크가 숨겨둔 전제 조건을 체크리스트로 관리하는 방식이 도움이 됩니다.
결론: 해결의 핵심은 “모드 확정”과 “반환 타입 고정”
TS 5.6 데코레이터에서 Unable to resolve signature 를 만났을 때, 가장 빠른 해결 루트는 아래 두 가지입니다.
- 프로젝트가 레거시 데코레이터인지(대부분의 기존 프레임워크) 새 표준 데코레이터인지 먼저 확정하고 tsconfig를 그에 맞게 고정
- 데코레이터 팩토리는 반드시
MethodDecorator같은 정확한 타입을 반환하도록 명시
이 두 가지를 해도 특정 라이브러리에서만 계속 문제가 난다면, 그 시점부터는 “라이브러리 버전 조합” 또는 “해당 라이브러리가 어느 데코레이터 모델을 전제하는지”가 본질입니다.
타입 시스템이 모호한 경계를 허용하지 않는 방향으로 계속 진화하고 있기 때문에, 이런 류의 문제는 앞으로도 반복됩니다. 이럴 때는 원인을 감으로 찍기보다, 최소 재현과 시그니처 고정으로 범위를 줄이는 접근이 가장 비용 대비 효과가 좋습니다.