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으로 인한 서버 크래시 방지

다음엔 로깅 시스템과 통합해서 에러 모니터링을 강화할 예정이다.