미들웨어 심화 — 에러 핸들링과 커스텀 미들웨어
Express에서
async핸들러가 에러를 던지면 왜 서버가 조용히 죽을까요?
Express의 에러 처리는 동기 코드에서는 자연스럽게 동작하지만, async/await를 쓰는 순간 함정이 생깁니다. 이 차이를 모르면 프로덕션에서 unhandled rejection으로 프로세스가 종료됩니다.
개념 정의
에러 미들웨어 는 파라미터가 4개인 미들웨어 (err, req, res, next)입니다. Express는 파라미터 개수로 일반 미들웨어와 에러 미들웨어를 구분합니다.
기본 에러 흐름
Express의 에러 전파 방식을 단계별로 보겠습니다.
- 미들웨어나 핸들러에서 에러가 발생합니다.
next(err)를 호출하면 일반 미들웨어를 모두 건너뛰고 에러 미들웨어 로 직행합니다.- 에러 미들웨어가 없으면 Express 기본 핸들러가 500 응답을 보냅니다.
요청 → [미들웨어 A] → [미들웨어 B] → [에러 발생!]
│
next(err) ───────┘
│
[일반 미들웨어 C] ← 건너뜀 │
↓
[에러 미들웨어] → 응답
핵심은 next(err) 호출입니다. 인자 없이 next()를 호출하면 다음 일반 미들웨어로, 인자를 넣으면 에러 미들웨어로 점프합니다.
에러 미들웨어 작성법
에러 미들웨어는 반드시 4개의 파라미터 를 선언해야 합니다. 3개로 쓰면 Express가 일반 미들웨어로 인식합니다.
// 에러 미들웨어 — 반드시 (err, req, res, next) 4개
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error',
});
});
위 코드에서 next 파라미터를 사용하지 않더라도 선언은 해야 합니다. Express는 function.length로 파라미터 개수를 체크하기 때문입니다.
여러 에러 미들웨어 체이닝
에러 미들웨어도 next(err)로 다음 에러 미들웨어에 전달할 수 있습니다.
// 1단계: 로깅
app.use((err, req, res, next) => {
console.error(`[ERROR] ${err.message}`);
next(err); // ← 다음 에러 미들웨어로 전달
});
// 2단계: 응답
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
error: err.message,
});
});
비동기 에러의 함정
Express 4는 2014년에 설계되었습니다. async/await(ES2017)보다 3년 앞서기 때문에, 비동기 에러를 자동으로 잡지 못합니다.
// 동기 에러 — Express가 자동으로 잡음
app.get('/sync', (req, res) => {
throw new Error('동기 에러'); // ← 에러 미들웨어로 전달됨
});
// 비동기 에러 — Express가 못 잡음!
app.get('/async', async (req, res) => {
throw new Error('비동기 에러'); // ← UnhandledPromiseRejection!
});
async 함수가 던진 에러는 Promise rejection이 됩니다. Express 4는 이 rejection을 감지하지 못하고, 에러 미들웨어로 전달하지 않습니다. 결과적으로 클라이언트는 응답을 받지 못하고, 서버 로그에 UnhandledPromiseRejection 경고만 남습니다.
Express 5(베타)에서는 async 에러를 자동으로 잡아줍니다. 하지만 아직 정식 릴리스 전이므로, Express 4에서는 직접 처리해야 합니다.
해결법 1: try-catch 래핑
app.get('/users', async (req, res, next) => {
try {
const users = await User.findAll();
res.json(users);
} catch (err) {
next(err); // ← 명시적으로 에러 미들웨어에 전달
}
});
동작하지만, 모든 라우트 핸들러마다 try-catch를 쓰면 코드가 반복됩니다.
해결법 2: 래퍼 함수
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.findAll();
res.json(users);
}));
asyncHandler가 Promise의 rejection을 잡아서 next(err)로 전달합니다. 라우트마다 try-catch를 쓸 필요가 없어집니다.
해결법 3: express-async-errors
가장 간결한 방법입니다. require만 하면 됩니다.
require('express-async-errors'); // ← 이 한 줄이면 끝
const express = require('express');
const app = express();
app.get('/users', async (req, res) => {
const users = await User.findAll(); // 에러 시 자동으로 next(err)
res.json(users);
});
이 라이브러리는 Express의 Layer.handle 메서드를 몽키패칭하여 async 함수의 반환값(Promise)에 .catch(next)를 자동으로 붙입니다.
커스텀 미들웨어 패턴
요청 시간 측정
function responseTime(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} - ${duration}ms`);
});
next();
}
app.use(responseTime);
res.on('finish')는 응답이 완전히 전송된 후에 실행됩니다. 이 이벤트를 활용하면 실제 응답 시간을 정확하게 측정할 수 있습니다.
인증 미들웨어
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
req.user = jwt.verify(token, SECRET);
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
}
// 특정 라우트에만 적용
app.get('/profile', authenticate, (req, res) => {
res.json(req.user);
});
미들웨어에서 req 객체에 속성을 추가하면 이후 핸들러에서 사용할 수 있습니다.
주의할 점
파라미터 개수를 틀리면 에러 미들웨어가 안 된다
// 3개 파라미터 — 일반 미들웨어로 인식됨!
app.use((err, req, res) => {
res.status(500).json({ error: err.message });
});
Express는 function.length === 4인 경우에만 에러 미들웨어로 인식합니다. 구조 분해나 기본값을 사용하면 length가 달라질 수 있으니 주의해야 합니다.
에러 미들웨어 위치는 반드시 마지막
// 잘못된 순서 — 에러 미들웨어가 라우트보다 위에 있음
app.use((err, req, res, next) => { /* ... */ });
app.get('/users', handler); // ← 여기서 발생한 에러를 잡지 못함
에러 미들웨어는 모든 라우트와 미들웨어 등록 이후에 배치해야 합니다.
정리
| 항목 | 설명 |
|---|---|
| 에러 미들웨어 | (err, req, res, next) — 반드시 파라미터 4개 |
| next(err) | 일반 미들웨어 건너뛰고 에러 미들웨어로 직행 |
| async 에러 | Express 4는 자동 캐치 불가, 래퍼 또는 express-async-errors 필요 |
| 등록 위치 | 에러 미들웨어는 모든 라우트 뒤, 가장 마지막 |
| Express 5 | async 에러 자동 처리 예정 (베타 단계) |