프론트엔드

Typescript 데코레이터로 NestJS처럼 Logger와 Validator 구현하기

2026. 01. 08. 목요일 오전 8시 11분

데코레이터(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 명령어로 테스트를 실행해보세요.