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