데코레이터는 @name으로 클래스나 클래스 멤버에 붙이는 함수입니다. 원래 메서드(또는 클래스, 필드)와 그것을 설명하는 컨텍스트 객체를 받고, 대체물을 반환할 수 있습니다. TypeScript는 컴파일러 플래그 없이 표준 데코레이터를 지원합니다.
@logged는 클래스가 정의될 때 한 번 실행되고, add를 자신이 반환한 래퍼로 대체합니다. 모든 호출은 래퍼를 거칩니다. 제네릭 매개변수가 메서드의 this, 인수, 반환 타입을 그대로 유지하므로 add는 여전히 숫자 두 개를 받고 숫자를 반환합니다.
데코레이터가 컴파일되는 방식
표준 데코레이터는 JavaScript의 TC39 제안에서 왔고, TypeScript는 TypeScript 5.0부터 이를 구현했습니다. 이 제안은 아직 JavaScript 표준에 포함되지 않았고 Node 24는 @ 문법을 파싱하지 못하므로, target이 ES2022일 때(이 페이지들처럼) 컴파일러는 꾸며진 각 클래스를 헬퍼 함수(__esDecorate와 __runInitializers, 파일 맨 위에 출력됨)를 호출하는 평범한 JavaScript로 다시 씁니다. 출력은 ES2022가 실행되는 곳이면 어디서든 실행됩니다.
그 결과 하나: 데코레이터가 있는 파일은 타입만 제거하고 @는 그대로 두는 Node의 내장 타입 제거(node file.ts)로 실행할 수 없습니다. Node는 SyntaxError: Invalid or unexpected token으로 멈춥니다. 먼저 tsc나 번들러로 컴파일하세요.
데코레이터 종류와 시그니처
모든 표준 데코레이터는 (value, context) => replacement | void 모양입니다. value가 무엇이고 무엇을 반환할 수 있는지는 무엇을 꾸미느냐에 따라 다릅니다.
| 꾸미는 대상 | value | 컨텍스트 타입 | 반환값 |
|---|---|---|---|
| 클래스 | 클래스 | ClassDecoratorContext | 대체 클래스, 또는 없음 |
| 메서드 | 메서드 | ClassMethodDecoratorContext | 대체 메서드 |
| getter / setter | getter 또는 setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext | 대체 getter 또는 setter |
| 필드 | undefined | ClassFieldDecoratorContext | 초기값을 변환하는 함수 |
accessor 필드 | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
모든 컨텍스트 객체에는 kind, name, addInitializer가 있습니다. 클래스 멤버의 컨텍스트에는 static, private, 그리고 인스턴스에서 멤버를 읽기 위한 access 객체도 있습니다. 데코레이터는 정적 멤버와 #private 멤버에도 동작합니다.
데코레이터 팩토리
옵션을 넘기려면 데코레이터를 반환하는 함수를 작성하고 @ 자리에서 호출하세요. 이것이 데코레이터 팩토리입니다.
@retry(3)은 먼저 retry를 호출하고, 그것이 반환한 함수가 실제 데코레이터입니다. 데코레이터 여러 개는 쌓입니다. @a @b method()에서 b가 먼저 적용되고 a가 그 결과를 감쌉니다.
클래스 데코레이터와 addInitializer
클래스 데코레이터는 클래스 자체를 받습니다. 하위 클래스를 반환해 대체하거나, 아무것도 반환하지 않고 어딘가에 기록만 할 수 있습니다. context.addInitializer는 특정 시점에 실행할 코드를 등록합니다. 클래스 데코레이터라면 클래스가 완전히 정의된 직후, 메서드 데코레이터라면 각 인스턴스가 생성될 때입니다.
클래스 데코레이터는 클래스 위에(또는 export 뒤에) 둡니다. @bound가 없으면 this가 undefined이므로 loose()를 호출하면 예외가 납니다.
필드 데코레이터와 accessor 데코레이터
필드 데코레이터는 나중의 대입을 보거나 가로챌 수 없습니다. value는 undefined이고, 반환할 수 있는 것은 필드의 초기값을 변환하는 함수뿐입니다. 읽기와 쓰기를 가로채려면 필드를 accessor 키워드로 선언하세요. 그러면 private 저장소를 쓰는 getter와 setter 쌍이 되고, 그것을 꾸미면 됩니다.
accessor는 같은 제안의 일부입니다. #private 필드 위에 실제 getter와 setter를 출력하므로, p.price = -5가 데코레이터의 set을 거칩니다.
표준 데코레이터와 레거시 experimentalDecorators
TypeScript 5.0 전에 TypeScript에 있던 데코레이터는 experimentalDecorators로 켜는 초기 버전의 제안뿐이었습니다. 이 플래그는 아직 있으며, 컴파일러를 시그니처와 의미가 다른 레거시 모델로 전환합니다.
| 표준(플래그 없음) | 레거시(experimentalDecorators) | |
|---|---|---|
| 시그니처 | (value, context) | (target, propertyKey, descriptor) |
| 메서드를 바꾸는 방식 | 새 함수를 반환 | descriptor.value를 변경 |
| 매개변수 데코레이터 | 지원 안 함(TS1206) | 지원 |
emitDecoratorMetadata | 지원 안 함 | 지원(reflect-metadata로 런타임 타입 정보) |
accessor 데코레이터({ get, set, init }), addInitializer | 예 | 아니요 |
| 기반 | TC39 제안 | 그 제안의 예전 초안 |
한 모델용으로 작성한 데코레이터는 다른 모델에서 타입 검사를 통과하지 못합니다. 플래그가 없는 프로젝트의 레거시 스타일 데코레이터입니다.
index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
해결책은 이 페이지의 첫 예제처럼 표준 (value, context) 형태로 다시 쓰거나, 프로젝트 전체에 레거시 모델을 켜는 것입니다.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS, TypeORM은 여전히 도구가 생성하는 설정에 experimentalDecorators를 넣고, 문서에서도 요구합니다. NestJS와 TypeORM은 런타임에 타입을 읽기 때문에 emitDecoratorMetadata도 필요합니다. NestJS는 생성자 매개변수를 주입하기 위해, TypeORM은 속성을 열에 매핑하기 위해서입니다.
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
이런 프레임워크를 쓴다면 레거시 방식으로 데코레이터를 쓰고 그 문서를 따르세요. 그런 프레임워크 없는 새 코드라면 표준 데코레이터를 쓰세요.
데코레이터를 언제 쓸까
데코레이터는 그러지 않으면 여러 메서드에 반복될 횡단 관심사에 맞습니다. 로깅, 시간 측정, 캐싱, 재시도, 접근 검사, 검증, 그리고 컨테이너나 라우터에 클래스를 등록하는 일입니다. 제어 흐름을 숨기기도 합니다. 메서드가 무엇을 하는지 알기 전에 @retry가 무엇을 하는지 찾아봐야 하기 때문입니다. 한두 번 쓸 거라면 평범한 고차 함수(const fetchData = retry(3, rawFetch))가 더 단순하고 클래스 밖에서도 동작합니다.
자주 묻는 질문
TypeScript에서 데코레이터란 무엇인가요?
데코레이터는 @name 문법으로 클래스나 클래스 멤버에 적용하는 함수입니다. 꾸밀 대상과 컨텍스트 객체를 받고, 대체물을 반환할 수 있습니다. 감싼 메서드, 새 클래스, 또는 필드의 초기값을 변환하는 함수입니다. 흔한 용도는 로깅, 검증, 캐싱, 클래스 등록입니다.
TypeScript에서 데코레이터를 쓰려면 experimentalDecorators가 필요한가요?
아니요. TypeScript 5.0부터 표준(TC39) 데코레이터는 플래그 없이 동작합니다. experimentalDecorators는 컴파일러를 예전의 레거시 데코레이터 모델로 전환하며, Angular와 NestJS 같은 프레임워크가 그 위에 만들어져 있습니다. 둘은 함수 시그니처가 달라서 서로 바꿔 쓸 수 없습니다.
표준 데코레이터와 experimentalDecorators의 차이는 무엇인가요?
표준 데코레이터는 (value, context)를 받아 대체물을 반환합니다. 레거시 데코레이터는 (target, propertyKey, descriptor)를 받아 속성 설명자를 변경합니다. 매개변수 데코레이터와 emitDecoratorMetadata는 레거시 모델만 지원하고, { get, set, init }을 반환하는 accessor 데코레이터와 context.addInitializer는 표준 모델에만 있습니다.
TypeScript는 매개변수 데코레이터를 지원하나요?
experimentalDecorators가 켜져 있을 때만 지원합니다. TC39 제안에 매개변수 데코레이터가 포함되지 않으므로, 표준 모드에서 매개변수에 붙인 데코레이터는 오류 TS1206: Decorators are not valid here입니다. 그래서 생성자 매개변수를 꾸미는 의존성 주입 프레임워크에는 레거시 플래그가 필요합니다.
데코레이터가 여러 개면 어떤 순서로 적용되나요?
데코레이터 표현식은 위에서 아래로 평가되지만, 적용은 아래에서 위로 됩니다. @a @b method()에서 b가 먼저 메서드를 감싸고 a가 그 결과를 감쌉니다. 그래서 메서드를 호출하면 a가 가장 바깥에서 실행됩니다.