reflect-metadata
메타데이터
메타데이터는 데이터에 대한 데이터다
간단하게 말하면 코드 자체에 대한 추가 정보를 붙여두는 느낌임
reflect-metadata를 사용하는 이유
자바스크립트 자체로는 메타데이터를 저장하고 조회할 수 있는 기능이 존재하지 않음
어떻게든 가능하긴 하지만 결국 우회책임
타입스크립트에서
type,interface등 으로 선언하는 타입정보는 컴파일 시점에 모두 사라짐reflect-metadat는 메타데이터를 저장하고 조회하는 표준화된 API를 제공해줌
ECMA 공식 표준은 아니지만 사실상 TS, Nest.js 등 생태계에서 널리 사용하고 있음
참고로
experimentalDecorators옵션을 활성화해야 사용이 가능함
사용해보기
클래스에 메타데이터 붙이기
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'메서드에 메타데이터 붙이기
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'프로퍼티에 메타데이터 붙이기
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' }파라미터에 메타데이터 붙이기
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
프로퍼티와 메서드의 타입을 가져올 수 있음
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의 타입: Functiondesign:paramtypes
파라미터의 타입 정보를 가져올 수 있음
Nest.js에서 DI를 위해서 해당 방법을 사용해서 처리함
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
반환 타입 정보를 가져옴
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처럼 리터럴 타입 정보가 손실됨
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제네릭과 유니온 타입의 상세정보
마찬가지로 상세한 정보가 유지되지 않음
이 경우 직접 메타데이터 정의를 통해서 해결해야함
함수를 메타데이터에 저장해 놨다가 나중에 인스턴스화를 하는 등 사용이 가능함
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]