자바스크립트와 메타프로그래밍

reflect-metadata 라이브러리

2025년 12월 11일8분1

reflect-metadata

메타데이터

  • 메타데이터는 데이터에 대한 데이터다

  • 간단하게 말하면 코드 자체에 대한 추가 정보를 붙여두는 느낌임

reflect-metadata를 사용하는 이유

  • 자바스크립트 자체로는 메타데이터를 저장하고 조회할 수 있는 기능이 존재하지 않음

    • 어떻게든 가능하긴 하지만 결국 우회책임

  • 타입스크립트에서 type, interface 등 으로 선언하는 타입정보는 컴파일 시점에 모두 사라짐

  • reflect-metadat는 메타데이터를 저장하고 조회하는 표준화된 API를 제공해줌

  • ECMA 공식 표준은 아니지만 사실상 TS, Nest.js 등 생태계에서 널리 사용하고 있음

  • 참고로 experimentalDecorators 옵션을 활성화해야 사용이 가능함


사용해보기

클래스에 메타데이터 붙이기

ts
import "reflect-metadata";

/**
 * basePath 메타데이터를 추가하는 데코레이터
 */
function Controller(path: string) {
  return function (target: Function) {
    Reflect.defineMetadata("basePath", path, target);
  };
}

@Controller("/seats")
class SeatController {
  // ...
}

const basePath = Reflect.getMetadata("basePath", SeatController);
console.log(basePath); // '/seats'

메서드에 메타데이터 붙이기

ts
import "reflect-metadata";

function Controller(path: string) {
  return function (target: Function) {
    Reflect.defineMetadata("basePath", path, target);
  };
}

function Get(path: string) {
  return function (target: any, methodName: string) {
    Reflect.defineMetadata("httpMethod", "GET", target, methodName);
    Reflect.defineMetadata("path", path, target, methodName);
  };
}

function Post(path: string) {
  return function (target: any, methodName: string) {
    Reflect.defineMetadata("httpMethod", "POST", target, methodName);
    Reflect.defineMetadata("path", path, target, methodName);
  };
}

@Controller("/seats")
class SeatController {
  @Get("/:seatId")
  getSeatStatus() {}

  @Post("/:seatId/start")
  startSession() {}
}

const httpMethod = Reflect.getMetadata("httpMethod", SeatController.prototype, "getSeatStatus");
console.log(httpMethod); // GET

const path = Reflect.getMetadata("path", SeatController.prototype, "getSeatStatus");
console.log(httpMethod, path); // 'GET', '/:seatId'

프로퍼티에 메타데이터 붙이기

ts
import "reflect-metadata";

function Column(options: { type: string; nullable?: boolean }) {
  return function (target: any, propertyName: string) {
    Reflect.defineMetadata("column", options, target, propertyName);
  };
}

class Seat {
  @Column({ type: "varchar" })
  seatNumber: string;

  @Column({ type: "datetime" })
  startedAt: Date;
}

const seatNumberColumn = Reflect.getMetadata("column", Seat.prototype, "seatNumber");
const startedAtColumn = Reflect.getMetadata("column", Seat.prototype, "startedAt");

console.log(seatNumberColumn); // { type: 'varchar' }
console.log(startedAtColumn); // { type: 'datetime' }

파라미터에 메타데이터 붙이기

ts
import "reflect-metadata";

function Body() {
  return function (target: any, methodName: string, paramIndex: number) {
    Reflect.defineMetadata("bodyParam", paramIndex, target, methodName);
  };
}

function Param(name: string) {
  return function (target: any, methodName: string, paramIndex: number) {
    const existingParams = Reflect.getMetadata("params", target, methodName) || [];
    existingParams.push({ index: paramIndex, name });
    Reflect.defineMetadata("params", existingParams, target, methodName);
  };
}

class SeatController {
  startSession(@Param("seatId") seatId: number, @Body() body: { userId: number; duration: number }) {
    // 좌석 사용 시작
  }
}

const params = Reflect.getMetadata("params", SeatController.prototype, "startSession");
console.log(params); // [{ index: 0, name: 'seatId' }]

const bodyIndex = Reflect.getMetadata("bodyParam", SeatController.prototype, "startSession");
console.log(bodyIndex); // 1

디자인 타임 메타데이터

  • emitDecoratorMetadata 옵션을 사용하는 경우 TS 컴파일러가 자동으로 타입 정보를 메타데이터에 저장함

  • 이걸 디자인 타임 메타데이터라고 부르며 데코레이터가 붙어있는 대상만 디자인 타임 메타데이터가 생성됨

design:type

  • 프로퍼티와 메서드의 타입을 가져올 수 있음

ts
import "reflect-metadata";

function Log(target: any, propertyKey: string) {
  const type = Reflect.getMetadata("design:type", target, propertyKey);
  console.log(`${propertyKey}의 타입:`, type?.name);
}

class Seat {
  @Log
  seatNumber: string;

  @Log
  isOccupied: boolean;

  @Log
  startedAt: Date;

  @Log
  startUsage() {}
}

// seatNumber의 타입: String
// isOccupied의 타입: Boolean
// startedAt의 타입: Date
// startUsage의 타입: Function

design:paramtypes

  • 파라미터의 타입 정보를 가져올 수 있음

  • Nest.js에서 DI를 위해서 해당 방법을 사용해서 처리함

ts
import "reflect-metadata";

function Injectable() {
  return function (target: Function) {
    const paramTypes = Reflect.getMetadata("design:paramtypes", target);
    console.log(
      `${target.name}의 생성자 파라미터:`,
      paramTypes?.map((t: any) => t.name)
    );
  };
}

class SeatRepository {}

class PaymentService {}

@Injectable()
class SeatService {
  constructor(private seatRepo: SeatRepository, private paymentService: PaymentService) {}
}

// SeatService의 생성자 파라미터: ['SeatRepository', 'PaymentService']

design:returntype

  • 반환 타입 정보를 가져옴

ts
import "reflect-metadata";

function LogReturn(target: any, methodName: string) {
  const returnType = Reflect.getMetadata("design:returntype", target, methodName);
  console.log(`${methodName}의 반환 타입:`, returnType?.name);
}

class Seat {
  id: number;
  name: string;
}

class SeatService {
  @LogReturn
  findById(id: number): Seat {
    return new Seat();
  }

  @LogReturn
  findAll(): Seat[] {
    return [];
  }

  @LogReturn
  async startSession(seatId: number): Promise<boolean> {
    return true;
  }
}

// findById의 반환 타입: Seat
// findAll의 반환 타입: Array
// startSession의 반환 타입: Promise

디자인 타임 메타데이터의 한계점

  • string[] | number[] 2개를 구분하지 못하고 항상 Array로만 나옴

  • seat: Seat | null인 경우 Object로 나오며 이는 유니온 타입에 대한 정보가 손실됨을 뜻함

  • active | deactive 처럼 리터럴 타입 정보가 손실됨

ts
import "reflect-metadata";

function Log(target: any, propertyKey: string) {
  const type = Reflect.getMetadata("design:type", target, propertyKey);
  console.log(`${propertyKey}의 타입:`, type?.name);
}

class Seat {
  id: number;
  name: string;
}

class Example {
  @Log
  items: string[];

  @Log
  data: Seat | null;

  @Log
  status: "active" | "inactive";
}

// findById의 반환 타입: Array
// findAll의 반환 타입: Object
// startSession의 반환 타입: String

제네릭과 유니온 타입의 상세정보

  • 마찬가지로 상세한 정보가 유지되지 않음

  • 이 경우 직접 메타데이터 정의를 통해서 해결해야함

  • 함수를 메타데이터에 저장해 놨다가 나중에 인스턴스화를 하는 등 사용이 가능함

ts
import "reflect-metadata";

function Type(type: Function) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata("customType", type, target, propertyKey);
  };
}

class Seat {
  id: number;
  name: string;
}

class Order {
  @Type(() => Seat)
  seats: Seat[];
}

const typeFunction = Reflect.getMetadata("customType", Order.prototype, "seats");
console.log(typeFunction); // [Function (anonymous)]
console.log(typeFunction()); // [class Seat]

댓글 0

ME

댓글을 작성하려면 로그인이 필요합니다

아직 댓글이 없습니다.

첫 번째 댓글을 작성해보세요!