Express의 미들웨어는 (req, res, next) 시그니처를 가진 함수로, 요청이 도착한 순간과 응답이 전송되는 순간 사이에 실행됩니다. 각 미들웨어는 req와 res를 읽거나 바꿀 수 있고, 응답을 보내 요청을 끝내거나, next()를 호출해 다음 함수에 요청을 넘길 수 있습니다. app.use(fn)은 모든 요청에 대한 미들웨어를 추가하고, Express는 등록한 순서대로 모두 실행합니다.
요청마다 Console에 세 단계가 순서대로 찍히고, 라우트는 로거가 추가한 속성인 req.startedAt을 읽습니다. GET /missing을 보내 보세요. app.use()는 모든 경로와 일치하므로 두 미들웨어는 여전히 로그를 남기고, 일치하는 라우트가 없어서 Express가 404로 응답합니다.
next()를 잊는 경우
응답도 보내지 않고 next()도 호출하지 않는 미들웨어는 요청을 붙잡아 둡니다. 클라이언트는 시간이 초과될 때까지 기다리고, 로그에는 아무것도 남지 않습니다. 그래서 이것은 흔한 미들웨어 버그입니다. 위의 logger에서 next();를 지우고 GET /를 다시 보내면 요청은 끝내 응답을 받지 못합니다.
규칙은 간단합니다. 미들웨어를 지나는 모든 경로는 정확히 다음 중 하나로 끝나야 합니다.
- 계속하려면
next() - 에러 핸들러로 건너뛰려면
next(err)(Express 에러 처리 참고) res.json()이나res.status(401).send()같은 응답
응답을 보낸 뒤 next()를 호출하는 것도 버그입니다. 다음 핸들러가 두 번째 응답을 보내려 하고 Express가 ERR_HTTP_HEADERS_SENT를 던집니다. 함수가 거기서 멈추도록 return res.status(401).json(...)이라고 쓰세요.
요청을 멈추는 인증 검사
요청을 거부하는 미들웨어는 직접 응답을 보내고 next()를 호출하지 않습니다. 라우트에 추가 인자로 넘기면 그 라우트만 보호할 수 있습니다.
첫 요청은 401과 {"error":"Missing or wrong API key"}를 받습니다. 요청 바에서 x-api-key: secret123 헤더를 추가하고 다시 보내면 이번에는 {"users":42,"caller":"reporting-app"}이 돌아옵니다. /public에서는 검사가 실행되지 않습니다.
미들웨어가 실행되는 범위
| 이렇게 쓰면 | 실행 대상 |
|---|---|
app.use(fn) | 모든 요청, 모든 메서드 |
app.use('/api', fn) | /api로 시작하는 경로 |
app.get('/admin', fn, handler) | GET /admin만, handler보다 먼저 |
router.use(fn) | 그 라우터의 모든 라우트 |
순서도 의미의 일부입니다. 라우트 뒤에 등록한 로거는 그 라우트가 응답한 요청을 절대 기록하지 않습니다. 응답을 보낸 라우트는 next()를 호출하지 않기 때문입니다. 로깅, 본문 파싱, CORS 같은 앱 전체 미들웨어를 먼저, 그다음 라우트, 마지막으로 404와 에러 핸들러를 등록하세요.
내장 미들웨어
Express에는 app.use()로 추가하는 미들웨어 함수가 몇 개 들어 있습니다. 본문 파서는 req.body를 채웁니다. 이것이 없으면 req.body는 undefined입니다.
| 미들웨어 | 하는 일 |
|---|---|
express.json() | JSON 본문(Content-Type: application/json) 파싱 |
express.urlencoded() | HTML 폼 전송(name=Ada&age=36) 파싱 |
express.text() | 일반 텍스트 본문을 문자열로 req.body에 넣음 |
express.static('public') | 폴더의 파일 제공 |
본문 {"name": "Ada"}로 POST /echo를 보내면 응답에 "body":{"name":"Ada"}가 들어 있습니다. express.json() 줄을 지우고 다시 보내면 응답에서 body가 사라집니다. req.body가 undefined이고 JSON.stringify는 undefined 값을 빼기 때문입니다. cors, helmet, morgan 같은 패키지도 미들웨어라서, npm install한 뒤 같은 방식으로 app.use()에 넘깁니다. 파일 제공은 express.static 페이지에서 다룹니다.
흔한 실수
미들웨어를 넘기지 않고 호출하는 것. app.use(logger())는 시작할 때 logger를 한 번 실행하고 그 반환값을 등록합니다. app.use(logger)라고 쓰세요(express.json()에 괄호가 붙는 것은 미들웨어 함수를 반환하기 때문입니다).
본문 파서를 라우트 뒤에 추가하는 것. 라우트가 먼저 실행되어 req.body를 undefined로 봅니다.
app.use('/admin', fn)이 경로 하나에만 일치할 거라고 기대하는 것. /admin/users를 비롯해 /admin 아래의 모든 경로에서, 모든 메서드에 대해 실행됩니다. 라우트 하나만 보호하려면 미들웨어를 app.get('/admin', fn, handler)에 넘기세요.
자주 묻는 질문
미들웨어와 라우트 핸들러의 차이는 뭔가요?
기술적으로는 없습니다. 둘 다 (req, res, next) 함수입니다. 관례상 라우트 핸들러는 응답으로 요청을 끝내고, 미들웨어는 보통 공통 작업을 한 뒤 next()를 호출합니다.
미들웨어를 async로 만들 수 있나요?
네. Express 5에서는 async 미들웨어가 에러를 던지거나 reject된 Promise를 반환하면 에러가 자동으로 에러 핸들러로 갑니다. 필요한 것을 await한 다음 next()를 호출하세요.
next('route')는 무엇을 하나요?
현재 라우트의 남은 핸들러를 건너뛰고 다음으로 일치하는 라우트로 넘어갑니다. app.get(), app.post() 같은 메서드 함수로 추가한 핸들러에서만 동작하고, app.use()에서는 동작하지 않습니다.
한 라우트에서만 미들웨어를 건너뛰려면?
그 라우트를 미들웨어보다 먼저 등록해 먼저 응답하게 하거나, 미들웨어 안에서 경로를 확인하세요(if (req.path === '/health') return next();). 앱 전체가 아니라 라우터나 라우트 단위로 미들웨어를 적용하면 아예 필요 없어지는 경우가 많습니다.