참고글 : https://javascript.plainenglish.io/how-to-write-simple-router-decorators-for-expressjs-with-typescript-3b8340b4d453
00. 들어가기 전
다른 글에서도 이야기한 적이 있지만 필자는 NodeJS -> Spring Boot -> NestJS 순서로 백엔드를 학습하여 현재는 Controller - Service - Repository - Domain 레이어 계층 구조로 서비스를 구성하는 것이 익숙해졌다.
주로 Node로 구성하게 되면 NestJS를 선택하지만 가끔은 Express를 사용하게 될 때가 있어 나는 Routing Controller를 이용하여 라우팅 처리를 하고 TypeDI를 사용하여 DI를 처리한다.
하지만 최근에 발견한 글이 흥미로워 정리를 할려고 한다. 외부 라이브러리 사용 없이 Routing Decorator를 구현하는 것이다. 해당 글은 위의 포스팅을 따라하는 것에 가까운 글이다.
01. 프로젝트 설정
빠르게 Express + Typescript를 이용한 프로젝트 셋팅을 진행하자.
pnpm init
pnpm install -D typescript ts-node @types/express reflect-metadata nodemon tsconfig-paths
pnpm install express
npx tsc -init
예제에서 제공해주는 tsconfig.json 파일인데 여기서 emitDecoratorMetadata, experimentalDecorator는 반드시 true로 설정하자.true로 설정해야 데코레이터 사용이 가능하다.
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"rootDir": "./src",
"outDir": "./dist",
"forceConsistentCasingInFileNames": true
}
}
그 후에 실행을 위하여 src 폴더에 app.ts와 server.ts를 구현한다.
// ** src/app.ts
import "reflect-metadata";
// ** Module Imports
import express from "express";
export class App {
public app: express.Application;
constructor() {
this.app = express();
this.setMiddlewares();
}
/**
* 미들웨어를 세팅한다.
*/
private setMiddlewares(): void {
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: false }));
}
/**
* Express를 시작한다.
* @param port 포트
*/
public async createExpressServer(port: number): Promise<void> {
try {
this.app.listen(port, "0.0.0.0", () => {
console.log(`Server is running on PORT : ${port} on ENV`);
});
} catch (error) {
console.error("Server start failed");
console.error(error);
}
}
}
// ** src/server.ts
import { App } from "./app";
try {
const app = new App();
const port = 3000;
app.createExpressServer(port);
} catch (error) {
console.error(error);
}
필자는 nodemon을 사용하여 서버를 킬 생각이기에 nodemon.json을 아래와 같이 구성한다.
{
"watch": ["./src"],
"ext": "ts",
"ignore": ["./test/*"],
"exec": "ts-node -r tsconfig-paths/register --transpile-only src/server.ts"
}
자 이제 아래 명령어를 터미널에 치면 서버가 가동된다.
pnpm nodemon
02. Decorator 구성하기.
자 이제 Decorator를 구성해보자. 먼저 Metadata Key부터 등록한다.
// ** /src/decorator/metadata.key.ts
export enum MetadataKeys {
BASE_PATH = "base_path",
ROUTERS = "routers",
}
그 후엔 해당 메타데이터를 이용하는 @Controller Decorator를 만든다.
// ** /src/decorator/controller.decorator.ts
import { MetadataKeys } from "./metadata.keys";
const Controller = (basePath: string): ClassDecorator => {
return (target) => {
Reflect.defineMetadata(MetadataKeys.BASE_PATH, basePath, target);
};
};
export default Controller;
그리고 이젠 라우터 Decorator를 생성하자. 필자는 자주 사용하는 GET, POST, PUT, DELETE, PATCH 5개의 메서드 Decorator를 생성하였다.
// ** /src/decorator/handlers.decorator.ts
import { MetadataKeys } from "./metadata.keys";
export enum Methods {
GET = "get",
POST = "post",
PUT = "put",
DELETE = "delete",
PATCH = "patch",
}
export interface IRouter {
method: Methods;
path: string;
handlerName: string | symbol;
}
const methodDecoratorFactory = (method: Methods) => {
return (path: string): MethodDecorator => {
return (target, propertyKey) => {
const controllerClass = target.constructor;
const routers: IRouter[] = Reflect.hasMetadata(
MetadataKeys.ROUTERS,
controllerClass
)
? Reflect.getMetadata(MetadataKeys.ROUTERS, controllerClass)
: [];
routers.push({
method,
path,
handlerName: propertyKey,
});
Reflect.defineMetadata(MetadataKeys.ROUTERS, routers, controllerClass);
};
};
};
export const Get = methodDecoratorFactory(Methods.GET);
export const Post = methodDecoratorFactory(Methods.POST);
export const Put = methodDecoratorFactory(Methods.PUT);
export const Delete = methodDecoratorFactory(Methods.DELETE);
export const Patch = methodDecoratorFactory(Methods.PATCH);
03. Decorator 사용하기.
자 이제 UserController를 만들어서 간단하게 Decorator를 사용해보자.
// ** /src/user.controller.ts
import { Request, Response } from "express";
import Controller from "./decorator/controller.decorator";
import { Get } from "./decorator/handlers.decorator";
@Controller("/user")
export default class UserController {
@Get("/")
public getUser(req: Request, res: Response) {
res.status(200).json({ message: "GET /user" });
}
}
이것만으로 코드가 돌아가면 좋겠지만, 우리가 만든 Controller들을 이용하여 라우터를 구성해야하기에 다시 server.ts에서 사용하는 Controller들을 순회하면서 사용한 Decorator를 기반으로 원하는 정보를 취득한 후 라우터에 등록하는 과정을 거쳐야한다.
나는 먼저 Controller들을 한 곳에서 관리할 controllers 파일을 생성했다.
// /src/decorator/controller.ts
// ** Controller Imports
import UserController from "../user.controller";
export const controllers = [UserController];
그리고 server.ts에서 Express가 가동될 때 등록할 수 있게한다.
// ** src/app.ts
import "reflect-metadata";
// ** Module Imports
import express, { Handler } from "express";
import { controllers } from "./decorator/controller";
import { MetadataKeys } from "./decorator/metadata.keys";
import { IRouter } from "./decorator/handlers.decorator";
export class App {
public app: express.Application;
constructor() {
this.app = express();
this.setMiddlewares();
this.registerRouters();
}
/**
* 미들웨어를 세팅한다.
*/
private setMiddlewares(): void {
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: false }));
}
/**
* Router를 등록한다.
*/
private registerRouters() {
const info: Array<{ api: string; handler: string }> = [];
controllers.forEach((controllerClass) => {
const controllerInstance: { [handleName: string]: Handler } =
new controllerClass() as any;
const basePath: string = Reflect.getMetadata(
MetadataKeys.BASE_PATH,
controllerClass
);
const routers: IRouter[] = Reflect.getMetadata(
MetadataKeys.ROUTERS,
controllerClass
);
const exRouter = express.Router();
routers.forEach(({ method, path, handlerName }) => {
exRouter[method](
path,
controllerInstance[String(handlerName)].bind(controllerInstance)
);
info.push({
api: `${method.toLocaleUpperCase()} ${basePath + path}`,
handler: `${controllerClass.name}.${String(handlerName)}`,
});
});
this.app.use("/api" + basePath, exRouter);
});
}
/**
* Express를 시작한다.
* @param port 포트
*/
public async createExpressServer(port: number): Promise<void> {
try {
this.app.listen(port, "0.0.0.0", () => {
console.log(`Server is running on PORT : ${port} on ENV`);
});
} catch (error) {
console.error("Server start failed");
console.error(error);
}
}
}
그러면 이제 잘 구동이 될 것이다.
03. 마무리하며.
항상 주어진 라이브러리나 Decorator를 사용했기에 직접 Route Decorator를 구현해보자라는 생각을 안 해봤는 데 좋은 글을 발견하여 한층 더 많은 것을 알게 되어 좋은 거 같지만 아직은 이해하는 정도라 Decorator와 DI에 대해서 좀 더 깊은 이해를 하도록 노력을 해야할 거 같다.
일단은 Custom DI를 별도로 구현해보고 해당 Controller에도 주입이 가능하게 수정해보고자한다.
'B.E > Node JS' 카테고리의 다른 글
Express + routing-controllers + typedi를 사용한 서버 구축하기(1) - Routing Controller (0) | 2023.10.12 |
---|---|
[Node JS] Node JS란? (0) | 2022.11.14 |
[Node JS] typescript + sequelize + passport로 유저 API 개발 - Passport로 Local 로그인하기 (0) | 2022.09.29 |
[Node JS] typescript + sequelize + passport로 유저 API 개발 - Local User 생성 (0) | 2022.09.29 |
[Node JS] typescript + sequelize + passport로 유저 API 개발 - Sequelize 셋팅 (0) | 2022.09.29 |