-
Notifications
You must be signed in to change notification settings - Fork 0
타입스크립트 Decorator 이해하기
Decorator는 타입스크립트의 강력한 기능 중 하나로, 코드의 가독성과 유지보수성을 향상시키며, 중복된 로직을 감소시키는 데 사용하는 도구입니다. Decorator는 객체 지향 프로그래밍과 AOP(Aspect-Oriented Programming)와 밀접한 관련이 있으며, 다양한 프레임워크와 라이브러리에서 활발하게 활용되고 있습니다. 이 글에서는 Decorator의 기본 개념과 주요 목적, Decorator 팩토리, Decorator 합성, Decorator 타입, 사용 법을 이야기해 보겠습니다.
Decorator 패턴이란 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴입니다. 즉 기본 기능에 추가할 기능이 많은 경우 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 경우 Decorator 객체로 조합해서 사용하는 설계 방식입니다.
타입스크립트 Decorator는 클래스, 메서드, 접근자, 프로퍼티, 매개변수 등의 맴버에 메타데이터를 추가하고, 동작을 변경하거나 확장하는데 사용되는 기능입니다.
Decorator는 @expression
형식을 사용합니다. 여기서 expression은 데코레이팅 된 선언에 대한 정보와 함께 런타임에 호출되는 함수여야 합니다. 즉 @expression
은 클래스, 메서드, 접근자, 프로퍼티, 매개변수 등의 선언 앞에 위치시킴으로써 사용됩니다.
function expression(target) {
// 'target'를 이용해서 수행
}
@expression
class Example {}
이렇게 Decorator 함수는 대상이 되는 클래스, 메서드, 접근자, 프로퍼티, 매개변수 등의 메타데이터를 받아 작업을 수행합니다.
- 관심사 분리 (Separation of Concerns): 데코레이터를 사용하면 코드의 관심사를 분리하여 모듈화할 수 있습니다. 특정 기능, 로직 또는 관심 분야를 독립적인 데코레이터로 정의하고, 이를 적용할 대상(클래스, 메서드, 프로퍼티)에 연결할 수 있습니다. 이로써 코드의 가독성과 유지보수성이 향상됩니다.
- 재사용성: 데코레이터를 사용하면 동일한 데코레이터를 여러 다른 클래스 또는 메서드에 적용할 수 있으며, 코드 중복을 줄이고 재사용성을 높일 수 있습니다. 특히, 공통된 기능(예: 로깅, 캐싱, 보안)을 여러 다른 컴포넌트에 쉽게 적용할 수 있습니다.
- 연산의 조합: 데코레이터를 조합하여 다양한 동작을 구현할 수 있습니다. 여러 데코레이터를 함께 사용하여 복잡한 동작을 구성하고, 필요에 따라 동작을 추가하거나 제거할 수 있습니다.
- 오픈/폐쇄 원칙 준수: 데코레이터는 소프트웨어 개발의 SOLID 원칙 중 하나인 "개방/폐쇄 원칙(Open/Closed Principle)"을 준수하는 방법으로 사용됩니다. 이 원칙에 따르면 소프트웨어 엔티티(클래스, 모듈 등)는 확장에는 열려 있어야 하고, 수정에는 폐쇄적이어야 한다고 정의합니다. 데코레이터를 사용하면 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있습니다.
- 메타데이터 추가: 데코레이터는 클래스, 메서드, 프로퍼티 등에 메타데이터를 추가하는 데 사용됩니다. 이 메타데이터는 런타임에 읽을 수 있으며, 예를 들어 Angular 프레임워크에서 컴포넌트, 서비스, 라우터 등을 정의할 때 사용됩니다.
- 횡단 관심사(Cross-Cutting Concerns) 처리: AOP의 일부로 사용될 때, 데코레이터는 횡단 관심사(로깅, 보안, 트랜잭션 관리 등)를 처리하는 데 유용합니다. 이러한 관심사는 애플리케이션의 여러 부분에 영향을 미치며, 데코레이터를 사용하면 중복 코드를 방지하고 한 곳에서 관리할 수 있습니다.
- 가독성 향상: 데코레이터를 사용하면 코드에 의도가 명확히 드러나고, 코드의 가독성이 향상됩니다. 어떤 동작이 어떤 데코레이터를 통해 적용되는지 명시적으로 나타낼 수 있습니다.
데코레이터 팩토리 함수는 데코레이터를 감싸는 래퍼 함수입니다. 데코레이터가 선언에 적용되는 방식을 원하는 대로 바꾸고 싶다면 데코레이터 팩토리를 다음과 같이 작성할 수 있습니다.
function factory(param1: any) { //데코레이터 팩토리
return function(target) { //데코레이터
// 'target'과 'param1'을 이용해서 수행
}
}
데코레이터 팩토리는 단순히 데코레이터가 런타임에 호출할 표현식을 반환하는 함수입니다.
데코레이터는 하나의 타겟의 여러 데코레이터를 적용할 수 있습니다. 이를 데코레이터 합성이라고 합니다. 다음 예제와 같이 여러 데코레이터를 적용할 수 있습니다.
function first() {
console.log("first(): factory 평가됨");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): 호출됨");
}
}
function second() {
console.log("second(): factory 평가됨");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): 호출됨");
}
}
class Example {
@first()
@second()
method(){}
}
출력 결과
"first(): factory 평가됨"
"second(): factory 평가됨"
"second(): 호출됨"
"first(): 호출됨"
출력 결과를 보면 데코레이터 표현식은 위에서 아래로 평가되고, 데코레이터 함수는 아래에서 위로 호출되는 것을 볼 수 있다.
타입스크립트에서 데코레이터의 타입은 데코레이터 함수가 받는 인자의 형태와 반환 값에 관련이 있습니다. 데코레이터는 클래스, 메서드, 접근자, 프로퍼티 또는 매개변수에 적용할 수 있으며, 각각에 대한 데코레이터 타입이 조금씩 다릅니다.
클래스 데코레이터는 클래스 선언 앞에 위치합니다. 생성자에 적용되며 클래스 정의를 관찰, 수정 또는 교체하는 데 사용할 수 있습니다.
클래스 데코레이터 표현식은 데코레이팅된 클래스의 생성자를 유일한 인수로 런타임에 함수로 호출됩니다.
클래스 데코레이터가 값을 반환하면 데코레이팅된 클래스가 수정, 확장됩니다. (반환 값은 새로운 클래스)
-
- 아래 코드는 클래스 맴버를 동적으로 확장하는 코드입니다.**
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
member1 = '새로운 값으로 덮어씌움';
member2 = 'new memeber'; // 새로운 맴버 추가
};
}
@classDecorator
class Example {
member1: string;
constructor(m: string) {
this.member1 = m;
}
}
let t = new Example('기존 값');
console.log(t);
console.log(t.member1); // 'override'
console.log((t as any).member2); //데코레이터로 확장된 속성은 타입 단언 필요!
class_1 { member1: '새로운 값으로 덮어씌움', member2: 'new memeber' }
새로운 값으로 덮어씌움
new memeber
-
- 아래 코드는 Decoraotr 팩토리를 이용한 예제입니다. **
function classDecorator(param1: string, param2: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
member1 = param1;
member2 = param2;
};
};
}
@classDecorator('새로운 값', 'member2')
class Example {
member1: string;
constructor(m: string) {
this.member1 = m;
}
}
let t = new Example('기존 값');
console.log(t);
console.log(t.member1); // 'override'
console.log((t as any).member2);
class_1 { member1: '새로운 값', member2: 'member2' }
새로운 값
member2
-
- 아래 코드는 프로토타입을 이용하여 확장하는 예제입니다. **
function classDecorator<T extends { new (...args: any[]): {} }>(constructorFn: T) {
// 프로토타입으로 새로운 프로퍼티를 추가
constructorFn.prototype.test2 = function () {
console.log('test2');
};
constructorFn.prototype.age = '26';
// 클래스를 프로퍼티에 상속시켜 새로운 멤버를 추가 설정
return class extends constructorFn {
public name = 'sjy';
constructor(...args: any[]) {
super(args);
}
public test1() {
console.log('test1');
}
};
}
@classDecorator
class Example {}
let e = new Example();
(e as any).test1(); //데코레이터로 동적으로 추가된 형태는 타입 단언 사용
(e as any).test2();
console.log((e as any).age);
console.log(e);
test1
test2
26
class_1 { name: 'sjy' }
메서드 데코레이터는 메서드 선언 앞에 위치합니다. 데코레이터는 메서드의 Property Descriptor에 적용되며 메서드 정의를 관찰, 수정 또는 대체하는 데 사용할 수 있습니다.
메소드 데코레이터 표현식은 런타임에 다음 세 개의 인수와 함께 함수로 호출됩니다.
첫 번째 인수는 정적 맴버에 대한 클래스의 생성자 함수, 인스턴스 맴버에 대한 클래스의 프로토타입 두 번째 인수는 메서드 이름 세 번째 인수는 메서드의 Property Descriptor
-
- 아래 코드는 데코레이팅된 메서드가 호출될 때 동작을 추가하는 예제입니다.**
function methodDecorator() {
return function (target: any, property: string, descriptor: PropertyDescriptor) {
// descriptor.value는 test() 함수를 가리킵니다.
let originMethod = descriptor.value;
// 기존의 test() 함수의 내용을 다음과 같이 바꿔줍니다.
descriptor.value = function (...args: any) {
console.log('동작 추가 1');
console.log("동작 추가 2");
originMethod.apply(this, args);
};
};
}
class Example {
@methodDecorator()
test() {
console.log("기존 동작");
}
}
let e = new Example();
e.test();
동작 추가 1
동작 추가 2
기존 동작
여기서 this로 originMethod를 apply로 연결해 준다. this로 연결해 주지 않으면 현재 실행되는 환경(컨텍스트)을 알 수 없기 때문에 originMethod가 제대로 동작하지 않는다.
프로퍼티 데코레이터는 포르퍼티 선언 앞에 위치합니다.
프로퍼티 데코레이터의 표현식은 런타임에 다음 두 개의 인수가 함께 함수로 호출됩니다.
첫 번째는 정적 맴버에 대한 클래스의 생성자 함수, 인스턴스 맴버에 대한 클래스의 프로토타입
두 번째는 프로퍼티 이름
-
- 아래 코드는 속성의 부가적인 설정을 하는 예제 입니다.**
function writable(writable: boolean) {
return function (target: any, decoratedPropertyName: any): any {
Object.defineProperty(target, decoratedPropertyName, {
writable,
});
};
}
class Example {
@writable(false)
data: number;
@writable(true)
data2: number;
constructor(num: number, num2: number) {
this.data = num;
this.data2 = num2;
}
}
let e = new Example(500, 1000);
e.data *= 2; //writable이 false이기 때문에 런타임 에러 발생
e.data2 *= 2;
매개변수 데코레이터는 매개 변수 선언 앞에 위치합니다.
매개변수 데코레이터 표현식은 런타임시 다음 세 개의 인수와 함께 함수로 호출됩니다.
첫 번째는 정적 맴버에 대한 클래스의 생성자 함수, 인스턴스 맴버에 대한 클래스의 프로토타입 두 번째는 파리미터 이름 세 번째는 파라미터의 순서 index ex) fn(p1, p2, p3) 일 때 p1은 0, p2는 1, p3은 2임
-
- 아래 코드는 파라미터 데코레이터를 사용하는 예시입니다. **
function parameterDecorator(target: any, paramName: string, paramIndex: number) {
console.log(paramName);
}
class Example {
test(@parameterDecorator m: string) {
console.log(m);
}
}
const e = new Example();
e.test("hi");
- property
- method
- parameter
- class
** 참고 문헌**