STUDY NOTE · 개념 정리
Express 미들웨어 정리: 실행 순서, next(), 에러 처리 미들웨어
Next/Express
Express 앱은 결국 요청이 차례로 통과하는 함수(미들웨어)들의 줄이다. 각 미들웨어는(req, res, next)를 받아 응답을 끝내거나,next()로 다음에 넘긴다. 등록한 순서가 곧 실행 순서이고, 인자가 4개인 미들웨어는 에러만 받는다.
1. 미들웨어의 모양
// 일반 미들웨어
app.use((req, res, next) => {
req.startedAt = Date.now();
next(); // 다음 미들웨어로
});
// 라우트 핸들러도 미들웨어의 한 종류 (보통 응답으로 끝냄)
app.get("/api/posts", (req, res) => {
res.json(posts);
});
// 에러 처리 미들웨어: 인자가 반드시 4개
app.use((err, req, res, next) => {
res.status(err.status ?? 500).json({ code: err.code ?? "INTERNAL" });
});
| 미들웨어가 할 수 있는 일 | 예 |
| req·res 고치기 | 파싱한 본문을 req.body에, 로그인 사용자를 req.user에 |
| 응답으로 끝내기 | res.json(), res.status(401).end() |
| 다음으로 넘기기 | next() |
| 에러로 넘기기 | next(err) → 일반 미들웨어는 건너뛰고 에러 미들웨어로 |
| 남은 라우트 미들웨어 건너뛰기 | next("route") (app.get 등 라우트 메서드에서만) |
2. 실행 순서
등록한 순서대로, 경로가 맞는 것만 실행된다. 그래서 순서가 곧 정책이다.
app.use(helmet()); // 1. 보안 헤더
app.use(express.json({ limit: "1mb" })); // 2. 본문 파싱 (이게 없으면 req.body가 undefined)
app.use(requestLog); // 3. 로그
app.use("/api", rateLimit); // 4. /api로 시작할 때만
app.use("/api/admin", requireAdmin); // 5. 관리자 확인
app.use("/api", apiRouter); // 6. 라우터
app.use((req, res) => res.status(404).json({ code: "NOT_FOUND" })); // 7. 아무것도 안 맞으면
app.use(errorHandler); // 8. 에러는 맨 마지막
| 실수 | 결과 |
| 인증 미들웨어를 라우터 뒤에 등록 | 이미 응답이 나가서 인증이 안 걸림 |
| express.json()을 라우터 뒤에 | 핸들러에서 req.body가 비어 있음 |
| 에러 미들웨어를 앞에 등록 | 뒤에서 난 에러를 못 받음 |
| next()도 안 하고 응답도 안 함 | 요청이 끝나지 않고 타임아웃까지 매달림 |
| 응답 후 next() 또 호출 | 다음 미들웨어가 또 응답하려다 "Cannot set headers after they are sent" |
3. 미들웨어의 범위
| 종류 | 등록 | 적용 범위 |
| 애플리케이션 수준 | app.use(fn), app.use("/path", fn) | 모든 요청, 또는 경로가 그 접두사로 시작하는 요청 |
| 라우터 수준 | router.use(fn) | 그 라우터에 들어온 요청만 |
| 라우트 수준 | app.get("/x", auth, handler) | 그 라우트만 (여러 개를 줄줄이 넣을 수 있음) |
| 내장 | express.json(), express.static() | - |
| 서드파티 | cors, helmet, morgan, cookie-parser | - |
app.use("/api", fn) 안에서 req.path는 /api를 뗀 나머지다. 원래 경로가 필요하면 req.originalUrl을 본다.
4. 에러 처리와 비동기
동기 코드에서 throw하면 Express가 잡아서 에러 미들웨어로 보낸다. Express 4에서는 async 함수가 reject돼도 잡지 못해서 직접 next(err)를 불러야 했다.
// Express 4: 감싸는 헬퍼가 필요했음
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/posts/:id", wrap(async (req, res) => {
res.json(await Post.findById(req.params.id));
}));
Express 5부터는 핸들러가 돌려준 Promise가 reject되면 자동으로 next(err)로 넘긴다. 그래서 async 핸들러에서 그냥 throw해도 된다.
에러 응답 원칙:
- 클라이언트에는 정해진 코드와 메시지만, 스택이나 내부 메시지는 서버 로그에만.
- 알고 던진 에러(400, 401, 404)와 예상 못 한 에러(500)를 클래스로 구분해 두면 에러 미들웨어가 단순해진다.
5. 미들웨어 vs 다른 프레임워크의 같은 개념
| 프레임워크 | 비슷한 개념 | 차이 |
| Spring | Filter(서블릿 단), Interceptor(핸들러 전후), AOP | 역할별로 층이 나뉘어 있음 |
| Next.js | middleware.ts (Edge에서 요청 전) | 라우트 앞 한 단계, 응답 가공보다는 리다이렉트·헤더용 |
| Koa | async 미들웨어, await next() | 다음 미들웨어가 끝난 뒤 돌아와서 후처리하는 '양파' 모델이 기본 |
Express도 res.on("finish", ...)를 걸면 응답이 끝난 뒤 시간을 잴 수 있다. 응답 시간 로깅을 이렇게 만든다.
6. 정리
- 미들웨어 =
(req, res, next). 응답으로 끝내거나next()로 넘기거나, 둘 중 하나는 꼭. - 등록 순서가 실행 순서: 보안 → 파싱 → 로그 → 제한 → 인증 → 라우터 → 404 → 에러.
- 에러 미들웨어는 인자 4개, 맨 마지막.
- Express 4는 async 에러를 직접 넘겨야 하고, Express 5는 자동으로 넘긴다.