데코레이터
데코레이터 (Decorators)
클래스에 부가 정보를 붙이거나 동작을 바꾸는 기능이 필요할 때가 있어요. 데코레이터는 클래스 선언과 멤버에 어노테이션(annotation) 과 메타프로그래밍 문법을 더하는 방법을 제공합니다. 클래스와 멤버를 꾸미고 수정하는 일이 훨씬 깔끔해지죠.
출처: TypeScript 핸드북
NOTE 이 문서는 실험적인 stage 2 데코레이터 구현을 다룹니다. Stage 3 데코레이터 지원은 TypeScript 5.0부터 사용할 수 있어요. 참고: TypeScript 5.0의 데코레이터
소개 (Introduction)
TypeScript와 ES6에서 클래스가 도입되면서, 클래스와 클래스 멤버를 어노테이션하거나 수정하는 지원이 필요한 시나리오가 생겼습니다. 데코레이터는 클래스 선언과 멤버에 어노테이션과 메타프로그래밍 문법을 모두 추가하는 방법을 제공해요.
추가 읽을거리 (stage 2): A Complete Guide to TypeScript Decorators
데코레이터의 실험적 지원을 켜려면 experimentalDecorators 컴파일러 옵션을 명령줄이나 tsconfig.json에서 활성화해야 해요.
명령줄 (Command Line):
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
데코레이터 (Decorators)
데코레이터 는 클래스 선언, 메서드, 접근자(accessor), 프로퍼티, 파라미터에 붙일 수 있는 특별한 종류의 선언입니다. 데코레이터는 @expression 형태를 쓰는데, expression은 런타임에 꾸며진(데코레이팅된) 선언에 대한 정보와 함께 호출될 함수로 평가되어야 해요.
예를 들어 @sealed 데코레이터가 있다면 sealed 함수를 다음과 같이 작성할 수 있어요.
function sealed(target) {
// do something with 'target' ...
}
데코레이터 팩토리 (Decorator Factories)
데코레이터가 선언에 적용되는 방식을 사용자화하고 싶다면, 데코레이터 팩토리를 작성할 수 있어요. 데코레이터 팩토리는 런타임에 데코레이터가 호출할 표현식을 반환하는 함수일 뿐입니다.
데코레이터 팩토리는 다음과 같이 작성할 수 있어요.
function color(value: string) {
// this is the decorator factory, it sets up
// the returned decorator function
return function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}
데코레이터 합성 (Decorator Composition)
하나의 선언에 여러 개의 데코레이터를 적용할 수 있는데, 예를 들어 한 줄에:
// @experimentalDecorators
// @noErrors
function f() {}
function g() {}
@f @g x
여러 줄에:
// @experimentalDecorators
// @noErrors
function f() {}
function g() {}
@f
@g
x
하나의 선언에 여러 데코레이터를 적용하면, 평가는 수학의 함수 합성과 비슷합니다. 이 모델에서 함수 f 와 g 를 합성하면, 합성 결과 (f ∘ g)(x)는 f(g(x))와 동일해요.
그래서 TypeScript에서 하나의 선언에 여러 데코레이터를 평가할 때는 다음 단계를 따릅니다.
- 각 데코레이터의 표현식은 위에서 아래로 평가된다.
- 결과는 아래에서 위로 함수로 호출된다.
데코레이터 팩토리를 쓰면 다음 예시로 이 평가 순서를 관찰할 수 있어요.
// @experimentalDecorators
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
이 예시는 콘솔에 다음 출력을 찍습니다.
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
데코레이터 평가 순서 (Decorator Evaluation)
클래스 안의 다양한 선언에 적용되는 데코레이터의 적용 순서는 명확히 정의되어 있어요.
- 각 인스턴스 멤버에 대해 파라미터 데코레이터 가 먼저, 그 다음 메서드/접근자/프로퍼티 데코레이터 가 적용된다.
- 각 static 멤버에 대해 파라미터 데코레이터 가 먼저, 그 다음 메서드/접근자/프로퍼티 데코레이터 가 적용된다.
- 생성자에 대해 파라미터 데코레이터 가 적용된다.
- 클래스에 대해 클래스 데코레이터 가 적용된다.
클래스 데코레이터 (Class Decorators)
클래스 데코레이터 는 클래스 선언 바로 앞에 선언합니다. 클래스 데코레이터는 클래스의 생성자에 적용되며, 클래스 정의를 관찰하거나, 수정하거나, 대체하는 데 쓸 수 있어요. 클래스 데코레이터는 선언 파일이나 다른 앰비언트 컨텍스트(예: declare 클래스)에서는 쓸 수 없습니다.
클래스 데코레이터의 표현식은 런타임에 함수로 호출되는데, 인자로 꾸며진 클래스의 생성자 하나만 받습니다.
클래스 데코레이터가 값을 반환하면, 그 값이 원래 클래스 선언을 제공된 생성자 함수로 대체합니다.
NOTE 새 생성자 함수를 반환하기로 했다면, 원래 프로토타입을 유지하도록 주의해야 해요. 런타임에 데코레이터를 적용하는 로직은 이를 대신 처리해 주지 않습니다.
BugReport 클래스에 적용된 클래스 데코레이터(@sealed)의 예시입니다.
// @experimentalDecorators
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
@sealed 데코레이터는 다음 함수 선언으로 정의할 수 있어요.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed가 실행되면 생성자와 그 프로토타입을 모두 봉인(seal)해서, BugReport.prototype에 접근하거나 BugReport 자체에 프로퍼티를 정의하는 방식으로 런타임에 이 클래스에 기능을 추가/제거하는 일을 막습니다(ES2015 클래스는 사실 프로토타입 기반 생성자 함수의 문법적 설탕이라는 점을 기억하세요). 이 데코레이터는 클래스가 BugReport를 서브클래싱하는 것은 막지 않습니다.
다음은 생성자를 오버라이드해 새 기본값(default)을 설정하는 예시입니다.
// @errors: 2339
// @experimentalDecorators
function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
reportingURL = "http://www...";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
bug.reportingURL;
메서드 데코레이터 (Method Decorators)
메서드 데코레이터 는 메서드 선언 바로 앞에 선언합니다. 메서드의 프로퍼티 디스크립터 에 적용되며, 메서드 정의를 관찰하거나 수정하거나 대체하는 데 쓸 수 있어요. 메서드 데코레이터는 선언 파일, 오버로드, 또는 다른 앰비언트 컨텍스트(예: declare 클래스)에서는 쓸 수 없어요.
메서드 데코레이터의 표현식은 런타임에 함수로 호출되는데, 다음 세 인자를 받습니다.
- static 멤버라면 클래스의 생성자 함수, 인스턴스 멤버라면 클래스의 프로토타입
- 멤버의 이름
- 멤버의 프로퍼티 디스크립터
NOTE 스크립트 target이
ES5보다 낮으면 프로퍼티 디스크립터 는undefined가 됩니다.
메서드 데코레이터가 값을 반환하면, 그 값이 메서드의 프로퍼티 디스크립터 로 사용됩니다.
NOTE 스크립트 target이
ES5보다 낮으면 반환 값은 무시됩니다.
Greeter 클래스의 메서드에 적용된 메서드 데코레이터(@enumerable) 예시입니다.
// @experimentalDecorators
function enumerable(value: boolean) {
return function (target: any,propertyKey: string,descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
@enumerable 데코레이터는 다음 함수 선언으로 정의할 수 있어요.
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
여기의 @enumerable(false) 데코레이터는 데코레이터 팩토리입니다. @enumerable(false)가 호출되면 프로퍼티 디스크립터의 enumerable 프로퍼티를 수정합니다.
접근자 데코레이터 (Accessor Decorators)
접근자 데코레이터 는 접근자 선언 바로 앞에 선언합니다. 접근자의 프로퍼티 디스크립터 에 적용되며, 접근자 정의를 관찰하거나 수정하거나 대체하는 데 쓸 수 있어요. 접근자 데코레이터는 선언 파일이나 다른 앰비언트 컨텍스트(예: declare 클래스)에서는 쓸 수 없어요.
NOTE TypeScript는 하나의 멤버에 대해
get과set접근자 둘 다를 데코레이팅하는 것을 허용하지 않습니다. 대신, 멤버의 모든 데코레이터는 문서 순서상 첫 번째 접근자에 적용되어야 해요. 데코레이터는get과set접근자를 각각이 아니라 결합한 프로퍼티 디스크립터 에 적용되기 때문입니다.
접근자 데코레이터의 표현식은 런타임에 함수로 호출되는데, 다음 세 인자를 받습니다.
- static 멤버라면 클래스의 생성자 함수, 인스턴스 멤버라면 클래스의 프로토타입
- 멤버의 이름
- 멤버의 프로퍼티 디스크립터
NOTE 스크립트 target이
ES5보다 낮으면 프로퍼티 디스크립터 는undefined가 됩니다.
접근자 데코레이터가 값을 반환하면, 그 값이 멤버의 프로퍼티 디스크립터 로 사용됩니다.
NOTE 스크립트 target이
ES5보다 낮으면 반환 값은 무시됩니다.
Point 클래스의 멤버에 적용된 접근자 데코레이터(@configurable) 예시입니다.
// @experimentalDecorators
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
@configurable 데코레이터는 다음 함수 선언으로 정의할 수 있어요.
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
프로퍼티 데코레이터 (Property Decorators)
프로퍼티 데코레이터 는 프로퍼티 선언 바로 앞에 선언합니다. 프로퍼티 데코레이터는 선언 파일이나 다른 앰비언트 컨텍스트(예: declare 클래스)에서는 쓸 수 없어요.
프로퍼티 데코레이터의 표현식은 런타임에 함수로 호출되는데, 다음 두 인자를 받습니다.
- static 멤버라면 클래스의 생성자 함수, 인스턴스 멤버라면 클래스의 프로토타입
- 멤버의 이름
NOTE TypeScript에서 프로퍼티 데코레이터가 초기화되는 방식 때문에, 프로퍼티 데코레이터에는 프로퍼티 디스크립터 가 인자로 전달되지 않습니다. 프로토타입의 멤버를 정의할 때 인스턴스 프로퍼티를 설명할 메커니즘이 현재 없고, 프로퍼티의 초기화자를 관찰하거나 수정할 방법도 없기 때문이에요. 반환 값도 무시됩니다. 따라서 프로퍼티 데코레이터는 특정 이름의 프로퍼티가 클래스에 선언됐다는 사실을 관찰하는 데만 쓸 수 있어요.
이 정보를 이용해 다음 예시처럼 프로퍼티에 대한 메타데이터를 기록할 수 있습니다.
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
그러면 @format 데코레이터와 getFormat 함수를 다음 함수 선언으로 정의할 수 있어요.
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
여기의 @format("Hello, %s") 데코레이터는 데코레이터 팩토리입니다. @format("Hello, %s")가 호출되면 reflect-metadata 라이브러리의 Reflect.metadata 함수를 사용해 프로퍼티에 메타데이터 항목을 추가하고, getFormat이 호출되면 format에 대한 메타데이터 값을 읽어옵니다.
NOTE 이 예시는
reflect-metadata라이브러리가 필요합니다.reflect-metadata라이브러리에 대한 자세한 내용은 Metadata를 참고하세요.
파라미터 데코레이터 (Parameter Decorators)
파라미터 데코레이터 는 파라미터 선언 바로 앞에 선언합니다. 클래스 생성자나 메서드 선언의 함수에 적용됩니다. 파라미터 데코레이터는 선언 파일, 오버로드, 또는 다른 앰비언트 컨텍스트(예: declare 클래스)에서는 쓸 수 없어요.
파라미터 데코레이터의 표현식은 런타임에 함수로 호출되는데, 다음 세 인자를 받습니다.
- static 멤버라면 클래스의 생성자 함수, 인스턴스 멤버라면 클래스의 프로토타입
- 멤버의 이름
- 함수의 파라미터 목록에서 파라미터의 순서 인덱스(ordinal index)
NOTE 파라미터 데코레이터는 메서드에 파라미터가 선언됐다는 것을 관찰하는 데만 쓸 수 있습니다.
파라미터 데코레이터의 반환 값은 무시됩니다.
BugReport 클래스의 멤버 파라미터에 적용된 파라미터 데코레이터(@required) 예시입니다.
// @experimentalDecorators
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) {}
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {}
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
그러면 @required와 @validate 데코레이터를 다음 함수 선언으로 정의할 수 있어요.
// @experimentalDecorators
// @emitDecoratorMetadata
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
@required 데코레이터는 파라미터를 필수로 표시하는 메타데이터 항목을 추가합니다. 그러면 @validate 데코레이터가 기존 print 메서드를, 원래 메서드를 호출하기 전에 인자를 검증하는 함수로 감쌉니다.
NOTE 이 예시는
reflect-metadata라이브러리가 필요합니다.reflect-metadata라이브러리에 대한 자세한 내용은 Metadata를 참고하세요.
메타데이터 (Metadata)
일부 예시는 실험적 메타데이터 API에 대한 폴리필을 추가하는 reflect-metadata 라이브러리를 사용합니다. 이 라이브러리는 아직 ECMAScript(JavaScript) 표준의 일부가 아니에요. 다만 데코레이터가 공식적으로 ECMAScript 표준의 일부로 채택되면, 이 확장들도 채택을 위해 제안될 것입니다.
이 라이브러리는 npm으로 설치할 수 있어요.
npm i reflect-metadata --save
TypeScript는 데코레이터가 있는 선언에 대해 일부 종류의 메타데이터를 내보내는 실험적 지원을 포함합니다. 이 실험적 지원을 켜려면 emitDecoratorMetadata 컴파일러 옵션을 명령줄이나 tsconfig.json에서 설정해야 합니다.
명령줄 (Command Line):
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
이 옵션이 켜지면, reflect-metadata 라이브러리가 import 되어 있는 한 추가적인 설계 시점(design-time) 타입 정보가 런타임에 노출됩니다.
이는 다음 예시에서 직접 확인할 수 있어요.
// @emitDecoratorMetadata
// @experimentalDecorators
// @strictPropertyInitialization: false
import "reflect-metadata";
class Point {
constructor(public x: number, public y: number) {}
}
class Line {
private _start: Point;
private _end: Point;
@validate
set start(value: Point) {
this._start = value;
}
get start() {
return this._start;
}
@validate
set end(value: Point) {
this._end = value;
}
get end() {
return this._end;
}
}
function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
let set = descriptor.set!;
descriptor.set = function (value: T) {
let type = Reflect.getMetadata("design:type", target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError(`Invalid type, got ${typeof value} not ${type.name}.`);
}
set.call(this, value);
};
}
const line = new Line()
line.start = new Point(0, 0)
// @ts-ignore
// line.end = {}
// Fails at runtime with:
// > Invalid type, got object not Point
TypeScript 컴파일러는 @Reflect.metadata 데코레이터를 사용해 설계 시점 타입 정보를 주입합니다. 이는 다음 TypeScript와 동등하다고 생각하면 돼요.
class Line {
private _start: Point;
private _end: Point;
@validate
@Reflect.metadata("design:type", Point)
set start(value: Point) {
this._start = value;
}
get start() {
return this._start;
}
@validate
@Reflect.metadata("design:type", Point)
set end(value: Point) {
this._end = value;
}
get end() {
return this._end;
}
}
NOTE 데코레이터 메타데이터는 실험적 기능이며 향후 릴리스에서 파괴적 변경(breaking changes)이 도입될 수 있습니다.
더 알아보기 (Learn more)
experimentalDecorators컴파일러 옵션emitDecoratorMetadata컴파일러 옵션- TypeScript 5.0의 데코레이터