오류 처리

오류 처리 (Error Handling)

오류 처리는 Express가 동기적·비동기적으로 발생하는 오류를 어떻게 잡아내고 처리하는지를 말해요. Express에는 기본 제공되는 오류 핸들러가 있어서 시작할 때 직접 작성하지 않아도 돼요. 라우트 핸들러와 미들웨어를 실행하는 동안 발생하는 모든 오류를 Express가 잡도록 보장하는 것이 중요해요.

출처: Express 공식 문서 - Error handling

오류 잡아내기

동기 코드의 오류

라우트 핸들러와 미들웨어의 동기 코드에서 발생한 오류는 추가 작업이 필요 없어요. 동기 코드가 예외를 던지면 Express가 스스로 잡아서 처리해요. 예를 들면:

app.get('/', (req, res) => {
  throw new Error('BROKEN'); // Express가 알아서 잡아줍니다
});

비동기 코드의 오류

비동기 핸들러를 작성하는 권장 방법은 async 함수를 쓰는 거예요. Promise를 반환하는 라우트 핸들러와 미들웨어는 reject되거나 예외를 던지면 자동으로 next(value)를 호출해요. async 함수는 항상 Promise를 반환하므로 별다른 작업 없이도 오류가 Express에 도달해요:

app.get('/user/:id', async (req, res) => {
  const user = await getUserById(req.params.id);
  res.send(user);
});

만약 getUserById가 오류를 던지거나 reject하면, 던져진 오류나 reject된 값과 함께 next가 호출돼요. next()에 무언가를 전달하면('route' 문자열 제외) Express는 현재 요청을 오류로 간주하고 남은 비-오류 처리 라우팅 및 미들웨어 함수를 건너뛰어요.

Promise 체인 사용하기

async 함수 대신 Promise 체인을 만들었다면, 핸들러에서 그 Promise를 반환해야 해요. 그래야 reject될 때 Express가 자동으로 next를 호출하지요:

app.get('/', (req, res) => {
  return Promise.resolve().then(() => {
    throw new Error('BROKEN'); // Express가 이를 잡아 next를 호출합니다
  });
});

Promise를 반환하지 않으면 Express는 그 존재를 알 수 없어요. 그 경우 직접 .catch(next)로 오류를 라우팅해야 해요. 그렇지 않으면 reject가 처리되지 않아 프로세스가 죽을 수 있어요:

app.get('/', (req, res, next) => {
  Promise.resolve()
    .then(() => {
      throw new Error('BROKEN');
    })
    .catch(next); // 오류가 Express로 전달됩니다
});

콜백 API 사용하기

node:fs 같은 콜백 기반 API가 만드는 오류는 던져지지도 Promise의 일부도 아니에요. 콜백이 첫 번째 인자로 그 오류를 받으므로, 직접 next() 함수로 전달해야 Express가 잡아서 처리해요:

app.get('/', (req, res, next) => {
  fs.readFile('/file-does-not-exist', (err, data) => {
    if (err) {
      next(err); // 오류를 Express로 전달합니다
    } else {
      res.send(data);
    }
  });
});

어떤 방법을 쓰든, Express 오류 핸들러가 호출되고 애플리케이션이 살아남기를 원한다면 Express가 오류를 반드시 받도록 해야 해요.

기본 오류 핸들러

Express에는 앱에서 만날 수 있는 오류를 처리해 주는 내장 오류 핸들러가 있어요. 이 기본 오류 처리 미들웨어는 미들웨어 스택의 끝에 추가돼요. next()에 오류를 전달했는데 커스텀 오류 핸들러에서 처리하지 않으면, 내장 핸들러가 처리하고 오류는 스택 트레이스와 함께 클라이언트에 기록돼요. 프로덕션 환경에서는 스택 트레이스가 포함되지 않아요. 프로덕션 모드로 실행하려면 환경 변수 NODE_ENVproduction으로 설정하세요.

응답에 오류가 기록될 때 다음 정보가 추가돼요: res.statusCodeerr.status(또는 err.statusCode)에서 설정되고, 4xx·5xx 범위 밖이면 500으로 설정돼요. res.statusMessage는 상태 코드에 따라 설정되고, 본문은 프로덕션이면 상태 코드 메시지의 HTML, 그 외에는 err.stack이 돼요.

응답을 이미 쓰기 시작한 뒤(예: 클라이언트로 스트리밍 중) 오류와 함께 next()를 호출하면, 기본 오류 핸들러는 연결을 닫고 요청을 실패시켜요. 그래서 커스텀 오류 핸들러를 추가할 때, 헤더가 이미 클라이언트로 전송됐다면 반드시 기본 Express 오류 핸들러에 위임해야 해요:

function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  res.status(500);
  res.render('error', { error: err });
}

오류 핸들러 작성하기

오류 처리 미들웨어는 다른 미들웨어와 같은 방식으로 정의하되, 인자가 3개가 아니라 4개 (err, req, res, next)라는 점이 핵심이에요:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

오류 처리 미들웨어는 다른 app.use()와 라우트 호출 뒤, 마지막에 정의해야 해요. 미들웨어 함수 내부의 응답은 HTML 오류 페이지, 단순 메시지, JSON 문자열 등 어떤 형식이어도 괜찮아요.

조직(및 상위 프레임워크) 목적으로 일반 미들웨어처럼 여러 오류 처리 미들웨어를 정의할 수도 있어요. 예를 들어 XHR 요청과 그렇지 않은 요청을 각각 처리하는 오류 핸들러를 만들 수 있지요:

app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

logErrors는 요청·오류 정보를 stderr에 기록할 수 있어요:

function logErrors(err, req, res, next) {
  console.error(err.stack);
  next(err);
}

clientErrorHandler에서는 오류를 명시적으로 다음 핸들러로 전달해요. 오류 처리 함수에서 next를 호출하지 않으면 응답을 직접 작성(그리고 종료)할 책임이 있다는 점을 기억하세요. 그렇지 않으면 요청이 "대기" 상태로 남아 가비지 컬렉션 대상이 되지 않아요:

function clientErrorHandler(err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' });
  } else {
    next(err);
  }
}

"전부 잡는" errorHandler는 다음과 같이 구현해요(예시):

function errorHandler(err, req, res, next) {
  res.status(500);
  res.render('error', { error: err });
}

여기서 next()next(err) 호출은 현재 핸들러가 끝났고 어떤 상태인지를 나타내요. next(err)는 위에서 설명한 오류 처리용으로 설정된 핸들러를 제외한 체인의 모든 나머지 핸들러를 건너뛰어요.

더 알아보기