카테고리 없음

Express로 나만의 Controller 만들기

코딩루이지 2023. 9. 23. 15:48

Intro

-- 개구리를 해부하지 말고, 개구리를 만들어라 -- 

But middleware, by its nature, is dumb. It doesn't know which handler will be executed after calling the next() function.
nestJS-Guards

nestJS에서도 express의 middleware는 멍청하다고 한다.

내가 나만의 router의 wrapper class인 Controller을 정의해보자.

 


개선하고싶은점

  1. middleware의 구분
    express의 middleware은 router랑 똑같이 생겨도 next 함수가 있으면 middleware로 동작한다.
    # Using middleware
    이를 명확하게 구분하자.

  2. 경로 고정
    그냥 라우터에는 첫 인자로 경로를 줄 경우 세부 경로가 지정된다. 하지만, 조금 더 명시적으로 하고싶었다.

  3. middleware의 명시적인 동작 순서
    middleware의 정의 순서는 예민해서 동작 순서에 맞게 잘 정의해야한다.
    middleware들의 정의들이 여기저기 발생하며 nestJS-Guards 문서처럼 next() 함수 이후 어떤게 실행될지 예측하기 어렵다.nestJS의 guard는 데코레이터를 활용해 선언적으로 middleware들을 정의했다.
    이처럼 선언적으로 이를 알아볼 수 있도록하자.
@Controller('cats') 
@UseGuards(RolesGuard) 
export class CatsController {}

 


 

개선하기

  1. 구조
    개선하고 싶다고 느끼고 부족하다고 느낀점은, 내가 그런부분들이 해결될 수 있도록 강제해야 겠다고 생각이 들었다.
    따라서, 상위 추상 class 를 만들거다.

  2. middleware 구분하기
    middleware를 구분하기 위해서는 next 함수를 인자로 받고 호출하는지 여부이다.
    middleware 가 아닌 일반 라우터일 경우 next를 인자로 받을 수 없도록 타입을 지정해주자.
export type RouterFunction = (req: Request, res: Response) => void;  

export type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => void;

 

  1. middleware 경로 명시적으로 동작 순서 나타타내기
    middleware의 동작 순서는 다음과 같다.
    예를들어 127.0.0.1/payment/history 에 get 요청을 보내면
  • / baseMiddlewares
  • /payment/history baseMiddlewares
  • /payment/history getMiddlewares
  • /payment/history get

순서로 함수들이 동작한다.

아래와 같이 코드가 작성되어있으면, log에 이런 메세지가 남는다.

baseMiddlewares 1

baseMiddlewares 2
history
baseMiddleware 1
history
baseMiddleware 2
history
getMiddleware 1
history
getMiddleware 2
history
get

controllers/indexController.ts

class IndexController extends Controller {  
    constructor(baseURL: string) {  
        super(baseURL);  

        this.baseMiddlewares = [];  

        this.baseMiddlewares.push((req, res, next) => {  
            console.log(baseURL);  
            console.log(`baseMiddlewares 1`);  
            next();  
        });  

        this.baseMiddlewares.push((req, res, next) => {  
            console.log(baseURL);  
            console.log(`baseMiddlewares 2`);  
            next();  
        });
...

controllers/payment/historyController.ts

class HistoryController extends Controller {  
    constructor(baseURL: string) {  
        super(baseURL);  

        this.baseMiddlewares = [];  

        this.baseMiddlewares.push((req, res, next) => {  
            console.log(baseURL);  
            console.log('baseMiddleware 1');  
            next();  
        })  
        this.baseMiddlewares.push((req, res, next) => {  
            console.log(baseURL);  
            console.log('baseMiddleware 2');  
            next();  
        })  

        this.getMiddlewares = [];  

        this.getMiddlewares.push((req, res, next) => {  
            console.log(baseURL);  
            console.log('getMiddleware 1');  
            next();  
        })  
        this.getMiddlewares.push((req, res, next) => {  
            console.log(baseURL);  
            console.log('getMiddleware 2');  
            next();  
        })  

        this.get = async (req: Request, res: Response) => {  
            console.log(baseURL);  
            console.log('get');
...

 

  1. 경로 고정하기
    express만 사용하면 경로 지정이 자유롭다. 각 controller가 하나의 경로만 담당하게 고정하고싶었다. 따라서, 경로 지정은 건들 수 없도록 하였다.

Controller.ts

export default abstract class Controller {  
    private readonly baseURL: string  
    constructor(baseURL: string) {  
        this.baseURL = '/' + baseURL;  
    }
...

private로 하여 상속받은 하위 class에서 접근하지 못하도록하고, readonly 옵션으로 수정이 불가능하게 하였다. 오로지 생성자 주입만 가능하도록 하였다.

  1. 사용 방법
    생성자에서 원하는 동작들을 등록하고, toRouter로 export 해주면 된다.
const historyController: HistoryController = new HistoryController('history');  

export default historyController.toRouter();

이렇게 하면 express처럼 import하여 router로 활용할 수 있다.

import historyRouter from "./historyController.js";