Published on

Express 미들웨어 스택 이해하기

라우팅 처리를 하는 코드에서 express와 미들웨어를 사용하고 있습니다. 저는 단순히 기존에 사용하던대로 사용하고 있었습니다.

// userAPI.ts
import express from 'express'
import router from 'express-promise-router'

export const userAPI = express()
const userRouter = router()
userAPI.use(userRouter)

// 인증 미들웨어
userRouter.use(verifyAuthenticationMiddleware)

const userController = new UserController(dependency.userService)

userRouter.get('/', userController.getUser)
userRouter.post('/', userController.createUser)

// 에러 핸들링 미들웨어
userRouter.use(defaultErrorHandler)

그런데 여기서 같은 메서드와 파라미터인데 path 파라미터 값에 따라 다른 컨트롤러의 함수를 호출하는 분기처리가 필요했습니다.

//  예를 들면 path 파라미터라 "batch"일 때 getUserBatch 메서드 호출
userRouter.get('/:id', userController.getUser)
userRouter.get('/batch', userController.getUserBatch)

일반적인 분기 처리는 조건문으로 가능하지만 라우터 내부에서 조건문을 사용하는 경우, 어떻게 해야할 지 고민이 되었습니다. 여기서 express 라우터와 미들웨어에 대한 이해가 필요했습니다.

미들웨어 스택 이해하기

Express 공식 문서의 "미들웨어 사용하기"에서 스택을 언급합니다.

미들웨어 함수는 다음과 같은 태스크를 수행할 수 있습니다. 모든 코드를 실행. 요청 및 응답 오브젝트에 대한 변경을 실행. 요청-응답 주기를 종료. 스택 내의 그 다음 미들웨어 함수를 호출. 현재의 미들웨어 함수가 요청-응답 주기를 종료하지 않는 경우에는 next()를 호출하여 그 다음 미들웨어 함수에 제어를 전달해야 합니다. 그렇지 않으면 해당 요청은 정지된 채로 방치됩니다.

자바스크립트에서는 스택 자료구조를 배열 형태로 사용할 수 있습니다. 미들웨어를 배열로 이해해봅시다. Express에서 미들웨어 사용은 use()메서드를 사용하고 있습니다. 메서드 사용은 스택 구조가 아니라 정확히 하나로 지정해서 사용하기 때문에 객체로 볼 수 있습니다. 정리하자면

use()는 배열에 요소를 추가, 라우터에서 메서드를 사용하면 객체로 추가

이 방식을 사용해서 위의 예시 코드인 userAPI.ts를 표현해보면 아래와 같습니다.

userAPI: [
  userRouter: [
    verifyAuthenticationMiddleware,
    {
      userController.getUser,
      userController.createUser
    },
    defaultErrorHandler
  ]
]

이제 전보다는 한 눈에 보기 쉬워졌습니다. 라우터와 미들웨어의 순서를 파악하기 쉽고 어떤 메서드가 어디에서 사용하는 지 보기 쉽습니다. 만약 더 다양한 라우터와 미들웨어를 사용한다면 이와 같은 형식으로 펼쳐보는 것이 더 유용할 것입니다.

이제 문제를 해결하기 위해 분기처리를 해봅시다. 이를 위해서는 우리가 라우터에서 사용하는 메서드의 타입을 알아야합니다. Express의 코드를 직접 보면 RequestHandler 타입임을 알 수 있습니다.

//
export interface RequestHandler<
  P = ParamsDictionary,
  ResBody = any,
  ReqBody = any,
  ReqQuery = ParsedQs,
  LocalsObj extends Record<string, any> = Record<string, any>
> {
  // tslint:disable-next-line callable-types (This is extended from and can't extend from a type alias in ts<2.2)
  (
    req: Request<P, ResBody, ReqBody, ReqQuery, LocalsObj>,
    res: Response<ResBody, LocalsObj>,
    next: NextFunction
  ): void
}

메서드 하나를 타입까지 명시하며 바꿔보면 아래와 같습니다.

userRouter.get('/:id', userController.getUser(req, res, next): void)

여기서 조건문을 사용하기 수월해집니다.

userRouter.get('/:id', (req, res, next): void => {
  if (req.params.id === 'batch') {
    await userController.getUserBatch(req, res, next)
  } else {
    await userController.getUser(req, res, next)
  }
})

패키지를 사용할 때도 기본적인 자료구조 형태와 크게 다르지 않음을 인지하고 더 이해하기 쉬운 형태로 이해하려는 방식은 좋은 것 같습니다. 앞으로도 활용해봐야겠습니다.