데코레이터(Decorator)란?
데코레이터는 클래스, 메서드, 속성, 또는 매개변수에 메타데이터를 추가하는 메타 프로그래밍 문법입니다. 기존 코드를 수정하지 않고 기능을 확장하거나 메타데이터를 추가할 수 있습니다.
데코레이터는 주로 다음과 같은 용도로 사용됩니다
- 로깅(logging)
- 유효성 검사(validation)
- 의존성 주입(dependency injection)
- 권한 검사(authorization)
데코레이터를 잘 활용하는 프레임워크로는 Angular, NestJS 등이 있습니다. Angular와 NestJS는 데코레이터를 통해 의존성 주입 및 유효성 검사, 권한 검사 등을 처리합니다.
직접 데코레이터 구현해보며 원리 이해하기
먼저, tsconfig.json 파일에서 experimentalDecorators 옵션을 활성화해야 합니다.
1{
2 "compilerOptions": {
3 "experimentalDecorators": true
4 }
5}
6데코레이터는 함수로 구현되며, 클래스나 메서드가 정의될 때 데코레이터 함수가 호출되어 메타데이터를 추가합니다.
1// simple-decorator.ts
2
3export function SimpleDecorator(
4 target: any, // 클래스 프로토타입 또는 생성자 함수
5 propertyKey?: string, // 속성 또는 메서드 이름
6 descriptor?: PropertyDescriptor | number, // 속성 설명자 또는 매개변수 인덱스
7) {
8 console.log('====================================================');
9 console.log('Target', target);
10 console.log('Property Key', propertyKey);
11 console.log('Descriptor', descriptor);
12}
13이제 이 데코레이터를 클래스, 메서드, 속성, 매개변수에 적용해보겠습니다.
1// example.ts
2
3import { SimpleDecorator } from './simple-decorator';
4
5@SimpleDecorator
6class Example {
7 @SimpleDecorator
8 private _myProperty01: string = '';
9
10 @SimpleDecorator
11 get property() {
12 return this._myProperty01;
13 }
14
15 @SimpleDecorator
16 foo(
17 @SimpleDecorator
18 name: string,
19 @SimpleDecorator
20 age: number,
21 ) {
22 return `Name: ${name}, Age: ${age}`;
23 }
24}
25위 코드를 실행하면, 각 데코레이터가 호출될 때마다 콘솔에 출력되는 내용을 확인할 수 있습니다. 예시로 메서드 데코레이터의 출력 결과를 확인해보겠습니다.
1// 메서드 데코레이터 출력결과
2// Target {}
3// Property Key foo
4// Descriptor {
5// value: [Function: foo],
6// writable: true,
7// enumerable: false,
8// configurable: true
9// }
10- target은 빈 객체로 출력되었는데, 실제로는 Example 클래스의 프로토타입을 가리킵니다.
- propertyKey는 메서드 이름인 'foo'가 출력됩니다.
- descriptor는 메서드의 속성 설명자로, 메서드의 구현체와 속성들을 포함합니다.
- 여기서 descriptor.value는 실제 메서드 함수입니다. 만약 데코레이터에서 이 값을 수정하면, 메서드의 동작을 변경할 수도 있습니다.
중간 정리
지금까지 데코레이터의 기본 개념과 구현 방법에 대해 알아보았습니다. 데코레이터를 간단하게 정리하면 다음과 같습니다.
- 데코레이터는 함수로 구현되며, 클래스, 메서드, 속성, 매개변수에 메타데이터를 추가하거나 기능을 확장하는 역할을 합니다.
- 데코레이터는 target, propertyKey, descriptor 등의 인자를 통해 데코레이터가 적용된 대상에 대한 정보를 제공합니다.
- 데코레이터를 활용하면 로깅, 유효성 검사, 의존성 주입 등 다양한 기능을 구현할 수 있습니다.
Logger 데코레이터 구현하기
이제 Logger 데코레이터를 구현해보겠습니다. 이 데코레이터는 메서드가 호출될 때마다 호출 정보를 로그로 출력합니다.
1export interface LoggerOptions {
2 mode: 'simple' | 'detailed';
3}
4
5export function Logger({ mode }: LoggerOptions = { mode: 'simple' }) {
6 return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
7 const originalMethod = descriptor.value;
8
9 descriptor.value = function (...args: any[]) {
10 console.log(`${propertyKey}를 호출합니다.`);
11 if (mode === 'detailed') {
12 console.log('매개변수:', args);
13 }
14
15 const result = originalMethod.apply(this, args);
16
17 console.log(`${propertyKey} 호출이 완료되었습니다`);
18 if (mode === 'detailed') {
19 console.log('반환값:', result);
20 }
21
22 return result;
23 };
24
25 return descriptor;
26 };
27}
28- 이번에는 데코레이터를 팩토리 함수로 구현했습니다. 이렇게 하면 데코레이터에 옵션을 전달할 수 있습니다.
- originalMethod 변수에 원본 메서드를 저장한 후, descriptor.value를 새로운 함수로 교체합니다. 이렇게 하면 메서드 호출 시 데코레이터에서 재정의한 함수가 실행됩니다.
- 새로운 함수에서는 메서드 호출 전후에 로그를 출력하고, 원본 메서드를 호출합니다.
로거를 적용한 예시 코드는 다음과 같습니다.
1import { Logger } from './logger';
2
3class Example2 {
4 @Logger({ mode: 'detailed' })
5 foo(name: string, age: number) {
6 return `Name: ${name}, Age: ${age}`;
7 }
8}
9
10const example2 = new Example2();
11const returnValue = example2.foo('Ina', 20);
12expect(returnValue).toBe('Name: Ina, Age: 20');
13
14// foo를 호출합니다.
15// 매개변수: [ 'Ina', 20 ]
16// foo 호출이 완료되었습니다
17// 반환값: Name: Ina, Age: 20
18- Example2 클래스의 foo 메서드에 @Logger 데코레이터를 적용했습니다.
- 메서드를 호출하면, 데코레이터에서 정의한 로그가 출력되는 것을 확인할 수 있습니다.
- mode 옵션에 따라 간단한 로그 또는 상세한 로그가 출력됩니다.
Validator(유효성 검사) 데코레이터 구현하기
이번에는 유효성 검사 데코레이터를 구현해보겠습니다. 이 데코레이터는 클래스 속성에 적용되어, 해당 속성의 값이 특정 조건을 만족하는지 검사합니다.
속성에 대한 유효성 검사 메타데이터를 저장하는 MinLength 데코레이터를 먼저 구현해보겠습니다.
1import 'reflect-metadata';
2
3export interface MinLength {
4 length: number;
5 message?: string;
6}
7
8/**
9 * 메타 정보를 저장하는 경우 Reflect API를 활용할 수 있다.
10 * reflect-metadata 패키지를 설치하고 import 해야 한다.
11 */
12export function MinLength({ length, message = '최소 길이는 ${length}입니다.' }: MinLength) {
13 return function (target: any, propertyKey: string) {
14 // 유효성 검사에 사용할 메타데이터 정의
15 const constraint = { value: length, message };
16
17 // Reflect API를 사용해 메타데이터 저장
18 Reflect.defineMetadata('minLength', constraint, target, propertyKey);
19 };
20}
21- MinLength 데코레이터는 속성에 적용되어, 해당 속성의 최소 길이를 지정합니다.
- 유효성 검사에 필요한 메타데이터를 저장해야 하는데, 이를 위해 reflect-metadata 패키지를 사용합니다.
- Reflect.defineMetadata 함수를 사용해 메타데이터를 저장합니다.
1
2npm install reflect-metadata
3이제 유효성 검사를 수행하는 validate 함수를 구현해보겠습니다.
1import 'reflect-metadata';
2
3export function validate(target: any) {
4 // 대상 객체의 각 속성에 대해 메타데이터 확인 및 유효성 검사 수행
5 for (const propertyKey of Object.keys(target)) {
6 // Reflect API를 사용해 유효성 검사를 위한 메타데이터 조회
7 const constraint = Reflect.getMetadata('minLength', target, propertyKey);
8
9 // 유효성 검사 메타데이터가 존재한다면,
10 if (constraint) {
11 const value = target[propertyKey];
12
13 // 유효성 검사 수행
14 if (typeof value === 'string' && value.length < constraint.value) {
15 const errorMessage = constraint.message.replace('${length}', constraint.value.toString());
16
17 console.log(errorMessage);
18 throw new Error(errorMessage);
19 }
20 }
21 }
22
23 console.log('유효성 검사를 통과했습니다.');
24}
25- 객체의 각 속성에 대해 Reflect.getMetadata 함수를 사용해 메타데이터를 조회합니다.
- 메타데이터가 존재하면, 해당 속성의 값을 검사하여 유효성 검사를 수행합니다.
- 유효성 검사에 실패하면 에러를 발생시키고, 성공하면 통과 메시지를 출력합니다.
이제 유효성 검사 데코레이터를 적용한 예시 코드를 살펴보겠습니다.
1import { MinLength, validate } from './validator';
2
3class Example {
4 @MinLength({ length: 3, message: '이름은 최소 ${length}자 이상이어야 합니다.' })
5 value: string;
6
7 constructor(value: string) {
8 this.value = value;
9 }
10}
11
12const example1 = new Example('Ina');
13validate(example1); // 유효성 검사를 통과했습니다.
14
15const example2 = new Example('MO');
16validate(example2); // ERROR! 이름은 최소 3자 이상이어야 합니다.
17- Example 클래스의 value 속성에 @MinLength 데코레이터를 적용했습니다.
- validate 함수를 호출하여 유효성 검사를 수행합니다.
- 첫 번째 예시에서는 'Ina'가 최소 길이 조건을 만족하여 통과 메시지가 출력됩니다.
- 두 번째 예시에서는 'MO'가 조건을 만족하지 않아 에러 메시지가 출력됩니다.
마치며
이번 포스트에서는 Typescript 데코레이터의 기본 개념과 구현 방법, 그리고 Logger와 Validator를 데코레이터로 구현해보았습니다. 데코레이터를 활용하면 코드의 가독성과 유지보수성을 높일 수 있으며, 다양한 기능을 손쉽게 추가할 수 있습니다. 앞으로도 데코레이터를 활용한 다양한 기능 구현에 도전해보시길 바랍니다!
본 포스트에서 다룬 코드는 데코레이터 예제 저장소에서 확인할 수 있습니다. pnpm install을 실행한 다음 pnpm test 명령어로 테스트를 실행해보세요.