Q. express.js의 라우터는 미들웨어입니다. 어떤 원리로 동작하기 때문에 미들웨어로 라우터를 구현할 수 있나요?
오늘은 질문에 답하기 위해, 미들웨어란 무엇인지와 함께 express에서 Router를 알아보려고 한다.
Using Middleware
Express 공식 홈페이지에서는 라우팅과 미들웨어 웹 프레임워크라고 express를 소개하고 있는데, express는 기본적으로 미들웨어 함수의 연속적인 호출을 처리하는 프레임워크다.
Middleware function은 어플리케이션의 request-response cycle에서 request object, response object, next middleware 함수에에 대한 액세스를 가진 함수를 가리키며, 아래의 네가지 기능을 수행할 수 있다. 만약, 현재의 middleware 함수에서 req-res 사이클을 종료하지 않으면, 다음 middleware 함수를 호출해야 한다. 아니라면 req가 처리되지 못하고 무한 대기를 타게 된다. (즉, middleware 체인 중 어디선가는 req를 처리해서 res를 날려줘야 끝난다.)
- 코드 실행
- request와 response 객체 수정
- request-response cycle 종료
- 스택에 다음 middleware 함수를 콜
Express에서 middleware는 아래와 같이 순차적으로 동작한다.

middleware에도 종류가 있는데, Express에서 middleware 종류는 아래와 같다.
- Application-level middleware
- Router-level middleware
- Error-handling middleware
- Built-in middleware
- Third-party middleware
Application-level middleware
앱레벨 미들웨어는 app.use(path, function)와 app.METHOD(path, function) 방식으로 사용 가능하다. (METHOD의 경우, get, post, put, delete 등과 같은 HTTP method를 말한다.)
- 아래 예제는 express 홈페이지에서 가져옴
const express = require('express')
const app = express()
/* path를 사용하지 않고, function만 사용한 경우 */
app.use((req, res, next) => {
console.log('Time:', Date.now())
next()
})
/* path를 사용한 경우 */
app.use('/user/:id', (req, res, next) => {
console.log('Request Type:', req.method)
next() // req-res 사이클 안에서 다음 미들웨어를 호출한다.
})
/* app.METHOD 방식을 사용한 경우 */
app.get('/user/:id', (req, res, next) => {
res.send('USER')
})
/* 미들웨어를 연속으로 호출 */
app.use('/user/:id', (req, res, next) => {
console.log('Request URL:', req.originalUrl)
next()
}, (req, res, next) => {
console.log('Request Type:', req.method)
next()
})
/* 현재 미들웨어의 남은 코드를 실행하지 않고, 다음 미들웨어로 호출 - next('route')로 호출 */
app.get('/user/:id', (req, res, next) => {
// if the user ID is 0, skip to the next route
if (req.params.id === '0') next('route')
// otherwise pass the control to the next middleware function in this stack
else next()
}, (req, res, next) => {
// send a regular response
res.send('regular')
})
// handler for the /user/:id path, which sends a special response
app.get('/user/:id', (req, res, next) => {
res.send('special')
})
/* 미들웨어는 array에 담아 재사용 가능 */
unction logOriginalUrl (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}
function logMethod (req, res, next) {
console.log('Request Type:', req.method)
next()
}
const logStuff = [logOriginalUrl, logMethod]
app.get('/user/:id', logStuff, (req, res, next) => {
res.send('User Info')
})
Router-level middleware
Router-level middleware는 express.Router() 인스턴스를 거쳐서 실행된다.
router 변수에 express.Router() 클래스를 할당하고, router 변수를 사용해 mountable route handler를 생성한다.
Router는 미들웨어이자 라우팅 시스템으로, "미니앱"이라고도 불린다.
라우터 레벨 미들웨어는 application-level middleware와 같은 방식으로 동작한다. 다만, 위에서 설명했듯, Router() 클래스에 의존해서 사용할 수 있다.
아래의 예제는, Router를 이용해서 개발한 router들을 하나의 모듈로 만들어 app에서 해당 모듈을 로드하는 방식이다.
// birds.js
const express = require('express')
const app = express()
const router = express.Router()
/* app.use() 대신 router.use()로 모두 동일하게 사용 가능 */
router.use((req, res, next) => {
console.log('Time:', Date.now())
next()
})
/* 라우터 레벨에서도 동일하게 미들웨어를 호출 할 수 있고,
next("route")를 사용해서 남은 코드를 수행하지 않고 다음 미들웨어로 넘길 수 있음 */
router.get('/user/:id', logStuff, (req, res, next) => {
const {id} = req.params
if (id === 1) next("route") // id가 ㅇ다음 미들웨어로 넘기기
res.send('User Info')
})
// define the about route
router.get('/about', (req, res) => {
res.send('About birds')
})
module.exports = router
//app.js
const birds = require('./birds')
// ...
app.use('/birds', birds)
Error-handling middleware
사실, express에는 빌트인 에러 핸들러가 존재한다. 이 디폴트 에러 핸들러 미들웨어는 미들웨어 콜 스택 마지막에 붙는다. 만약, 앞선 미들웨어서 발생한 에러를 next(err)로 전달하고, 커스텀 에러 핸들링을 하지 않는다면, 콜 스택 마지막에 깔려있는 에러 핸들러로 전달되어 처리되고, 클라에 stack trace에 포함되어 보여진다. (production 환경에서는 stack trace가 포함되지 않는다.)
에러를 디폴트 핸들러로 처리하면, 아래 정보가 response로 전달된다.
- The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
- The res.statusMessage is set according to the status code.
- The body will be the HTML of the status code message when in production environment, otherwise will be err.stack.
- Any headers specified in an err.headers object.
Error handler middleware 만들기
Error-handling middleware는 앞서 소개한 두가지 미들웨어와 같은 방식으로 동작한다. 다만, 에러 핸들링 미들웨어는 파라미터 값을 항상 네가지로 받아와야 한다. (err, req, res, next)
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
})
app에서 에러핸들러 미들웨어를 설정하려면, app.use(router 포함)를 먼저 정의하고, error handler middleware를 가장 마지막에 정의해야 한다.
const bodyParser = require('body-parser')
const methodOverride = require('method-override')
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use((err, req, res, next) => {
// logic
})
기능에 따라 에러 핸들러 미들웨어를 여러가지로 만들어 관리하는 방법
const bodyParser = require('body-parser')
const methodOverride = require('method-override')
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)
// 에러 stack을 출력하고, 다음 미들웨어(clientErrorHandler)로 에러를 넘김
function logErrors (err, req, res, next) {
console.error(err.stack)
next(err)
}
// req.xhr이 존재하면, res를 날리고 종결, 없으면 "catch-all" errorHandler로 에러를 넘김
function clientErrorHandler (err, req, res, next) {
if (req.xhr) {
res.status(500).send({ error: 'Something failed!' })
} else {
next(err)
}
}
// 이 단계로 에러가 넘어오면 무조건 아래와 같이 에러를 핸들링하고, req-res 사이클을 종결
function errorHandler (err, req, res, next) {
res.status(500)
res.render('error', { error: err })
}
Build-in middleware
4.x 버전 이상의 express에서는 더이상 Connect에 의존하지 않고, 아래 3가지 미들웨어를 빌트인으로 제공한다고 한다. (npm을 통해 모듈을 따로 설치하지 않아도, 사용이 가능하다는 의미다.)
- express.static serves static assets such as HTML files, images, and so on.
- express.json parses incoming requests with JSON payloads. NOTE: Available with Express 4.16.0+
- express.urlencoded parses incoming requests with URL-encoded payloads. NOTE: Available with Express 4.16.0+
아래는 기존 express 빌트인으로 기본 제공되던 미들웨어 중, 현재는 별도의 모듈로 제공되는 미들웨어의 리스트이다.
(기존에는 빌트인 제공이었지만, 현재는 third-party 미들웨어로 제공)
GitHub - senchalabs/connect: Connect is a middleware layer for Node.js
Connect is a middleware layer for Node.js. Contribute to senchalabs/connect development by creating an account on GitHub.
github.com
Thir-party middleware
cookie-parser와 같이, npm을 통해 별도로 설치를 해줘야 하는 express 외부 모듈을 말한다.
$ npm install cookie-parser
// 스크립트 파일에서 설치한 모듈을 불러와 사용
const express = require('express')
const app = express()
const cookieParser = require('cookie-parser')
// load the cookie-parsing middleware
app.use(cookieParser())
Express의 Router : 미들웨어로 라우터를 구현할 수 있는 이유
Router 인스턴스는 express에 포함된 빌트인 미들웨어로, 라우팅 시스템이다. router는 미들웨어 자체로 기능하며, 따라서 app.use() 또는 다른 router.use()의 argument로 사용이 가능하다.
최상위 레벨에 위차한 express 객체에는 Router() 메서드를 가지고 있고, 해당 메서드를 통해 새로운 router 객체를 만들어 낸다. router 객체를 만들고 나면, Router 인터페이스에 내장되어 있는 여러가지 기능을 사용할 수 있다.
Router()에서 제공하는 기능을 알고 싶어서, ctrl + 클릭을 해봤다.
명세서에는 Router는 IRouter를 extend 하고 있었다.
// core Router의 명세서
export interface Router extends IRouter {}
// IRouter의 명세서
export interface IRouter extends RequestHandler {
path: string;
stack: any;
all: IRouterHandler<this, Route>;
get: IRouterHandler<this, Route>;
post: IRouterHandler<this, Route>;
put: IRouterHandler<this, Route>;
delete: IRouterHandler<this, Route>;
patch: IRouterHandler<this, Route>;
options: IRouterHandler<this, Route>;
head: IRouterHandler<this, Route>;
...
use: IRouterHandler<this> & IRouterMatcher<this>;
route<T extends string>(prefix: T): IRoute<T>;
route(prefix: PathParams): IRoute; //IRoute 인터페이스는 HTTP 메서드를 사용하도록 함
...
}
IRouter 인터페이스의 "use" 속성을 보면, router.use("path", middleware)로 호출할 경우, IRouterHandler<this=path>, IRouterMatcher<this=middleware>로 처리하게 되는 것 같다.
- IRouterHandler 인터페이스는 "path"로 Route > IRoute 해주는 인터페이스 인것 같다.
- IRouterMatch 인터페이스는 middleware를 route > IRoute 메서드로 태워서 HTTP method를 실행해주는 것으로 이해된다.
- 즉, router.use("path", middleware) -> IRoute("path") & middleware = router.get("path", (req,res, next) => {}) 이런 식으로 호출이 이루어지는 것! 그리고, middleware에서 다시 next를 호출하고 그 뒤에 호출하고... 이런 식으로 연결연결 되는 것이다.
// IRouteHandler 명세
export interface IRouterHandler<T, Route extends string = string> {
(...handlers: Array<RequestHandler<RouteParameters<Route>>>): T;
(...handlers: Array<RequestHandlerParams<RouteParameters<Route>>>): T;
<
P = RouteParameters<Route>,
ResBody = any,
ReqBody = any,
ReqQuery = ParsedQs,
Locals extends Record<string, any> = Record<string, any>
>(
// tslint:disable-next-line no-unnecessary-generics (This generic is meant to be passed explicitly.)
...handlers: Array<RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>>
): T;
...
}
// IRouterMatcher 명세
export interface IRouterMatcher<
T,
Method extends 'all' | 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' = any
> {
<
Route extends string,
P = RouteParameters<Route>,
ResBody = any,
ReqBody = any,
ReqQuery = ParsedQs,
Locals extends Record<string, any> = Record<string, any>
>(
// tslint:disable-next-line no-unnecessary-generics (it's used as the default type parameter for P)
path: Route,
// tslint:disable-next-line no-unnecessary-generics (This generic is meant to be passed explicitly.)
...handlers: Array<RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>>
): T;
...
}
돌고 돌아 드디어 결론...
const router = express.Router()로 선언한 router 객체에 use 메서드를 사용하면, 파라미터로 받은 path와 middleware를 Route로 다시 호출하고, response를 날려서 req-res cycle이 종료, 또는 마지막 미들웨어에 닿아 콜 스택이 종료될때까지 next()를 호출한다. 그래서, express의 Router는 미들웨어이자, 라우터이다!!! (이게 말이 되나..???ㅋㅋㅋ)
'항해99_10기 > 105일의 TIL & WIL' 카테고리의 다른 글
| [TIL] [4주차] [20221208] 4주차 회고 - 코드리뷰와 리팩토링 / package.json, EC2 set locale, express sanitizer (1) | 2022.12.09 |
|---|---|
| [4주차] [20221208] node.js의 require() 작동 방식 (0) | 2022.12.08 |
| [4주차] [20221206] thunder client에서 req.cookies 넘기기 (0) | 2022.12.06 |
| [4주차] [20221205] Joi validation & Sequelize를 이용한 mySQL 설정 (0) | 2022.12.06 |
| [3주차 WIL] 2022.11.28 ~ 2022.12.03 회고 (0) | 2022.12.04 |