데코레이터와 reflect-metadata

정은영·2025년 6월 29일
1

CS

목록 보기
24/24

이번에 새롭게 라이브러리를 만드는 프로젝트에 참여하게 되었어요. 프로젝트 시작 전 TypeSrcript 데코레이터, reflect-metadata에 대해서 공부를 하려고 합니다.

데코레이터

데코레이터는 클래스, 속성, 메서드, 파라미터 등에 메타데이터를 추가하거나 동작을 바꾸는 함수입니다.

데코레이터의 종류

데코레이터 종류대상예시
클래스 데코레이터클래스 자체@Controller()
속성 데코레이터클래스 프로퍼티@body('username')
메서드 데코레이터클래스 메서드@Get()
파라미터 데코레이터메서드의 매개변수@Param('id')

데코레이터는 메타데이터만 붙인다!

데코레이터는 실제 동작을 바꾸지는 않습니다.

reflect-metadata를 써서 메타데이터를 붙여 놓고, 미들웨어나 프레임워크 코드가 나중에 그 정보를 읽어서 처리하는 구조입니다.

아래 코드와 같이 "name이라는 필드에는 req.body.username을 넣어줘야 해"라는 정보를 저장합니다.

Reflect.defineMetadata('cargo:body', [{ propertyKey: 'name', fieldName: 'username' }], target.constructor);

데코레이터가 적용되는 시점

데코레이터는 클래스가 정의될 때 실행됩니다. 인스턴스를 만들기 전에도 데코레이터는 이미 실행되어 있습니다.

reflect-metadata

데코레이터는 내부적으로 메타 데이터를 저장하지 않습니다. 따라서 저장하려면 reflect-metadata를 사용해야 합니다.

아래처럼 데코레이터에서 내에서 사용할 메타데이터를 런타임에서 조회해서 처리할 수 있습니다.

import 'reflect-metadata';

// 저장
Reflect.defineMetadata('key', 'value', target);

// 읽기
const value = Reflect.getMetadata('key', target);

데코레이터 만들기 예시(속성 데코레이터)

데코레이터 정의

데코레이터는 다음과 같이 함수입니다.

// body.decorator.ts
import 'reflect-metadata';

const BODY_METADATA_KEY = 'cargo:body';

export function body(fieldName: string): PropertyDecorator {
  return (target, propertyKey) => {
    const existing =
      Reflect.getMetadata(BODY_METADATA_KEY, target.constructor) || [];

    Reflect.defineMetadata(
      BODY_METADATA_KEY,
      [...existing, { propertyKey, fieldName }],
      target.constructor
    );
  };
}

DTO에 데코레이터 붙이기

// create-user.dto.ts
import { body } from './body.decorator';

export class CreateUserDto {
  @body('username')
  name!: string;

  @body('email')
  email!: string;
}

미들웨어에서 매핑

function bindingCargo(DtoClass: any) {
  return (req, res, next) => {
    const dto = new DtoClass();
    const bodyMeta = Reflect.getMetadata('cargo:body', DtoClass) || [];

    for (const { propertyKey, fieldName } of bodyMeta) {
      dto[propertyKey] = req.body?.[fieldName]; // 핵심!
    }

    (req as any).__cargo = dto;
    next();
  };
}

컨트롤러에서 사용

app.post('/user', bindingCargo(CreateUserDto), (req, res) => {
  const dto = getCargo<CreateUserDto>(req);
  res.json({ name: dto.name, email: dto.email });
});

내부 흐름

1) 클래스가 로딩 되는 시점(앱 로딩시)에 @body()가 실행되고 메타데이터가 저장됩니다.

  • 저장되는 메타데이터
[
  { propertyKey: 'name', fieldName: 'username' },
  { propertyKey: 'email', fieldName: 'email' }
]
  • cargo:body라는 키로 CreateUserDto 클래스에 저장됩니다.
Reflect.getMetadata('cargo:body', CreateUserDto);

// 결과
[
  { propertyKey: 'name', fieldName: 'username' },
  { propertyKey: 'email', fieldName: 'email' }
]

2) 요청이 들어오면 bindingCargo()가 메타데이터 조회해서 req.body에서 필요한 필드를 읽어서 DTO에 넣습니다.

  • 요청 바디가 아래와 같다면
{
  "username": "은영",
  "email": "ey@gmail.com"
}

다음과 같이 매핑됩니다.
dto.name → "은영"
dto.email → "ey@gmail.com"

3) 컨트롤러에서는 getCargo()로 DTO 꺼내서 사용합니다.

0개의 댓글