Node.js Express 미들웨어에서 에러 핸들링 구조 개선
문제 상황
기존 프로젝트의 라우트 핸들러마다 try-catch 블록이 반복되고, 에러 응답 형식도 제각각이었다. 특히 비동기 함수에서 발생한 에러가 제대로 캐치되지 않아 서버가 크래시되는 경우도 있었다.
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
모든 라우터에 이런 패턴이 반복되고 있었다.
해결 방법
1. async 핸들러 래퍼 함수
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
비동기 함수를 감싸서 에러를 자동으로 next()로 전달하도록 했다.
2. 커스텀 에러 클래스
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
3. 중앙 에러 핸들링 미들웨어
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: 'error',
message: err.message,
stack: err.stack
});
} else {
res.status(err.statusCode).json({
status: 'error',
message: err.isOperational ? err.message : 'Internal server error'
});
}
});
적용 후
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new AppError('User not found', 404);
}
res.json(user);
}));
코드가 훨씬 깔끔해지고, 에러 처리 로직이 한 곳에 모였다.
결과
- 라우터 코드 중복 제거
- 일관된 에러 응답 형식
- 프로덕션 환경에서 민감한 에러 정보 숨김 처리
- 미처리 Promise rejection으로 인한 서버 크래시 방지
다음엔 로깅 시스템과 통합해서 에러 모니터링을 강화할 예정이다.