express의 동기와 비동기 에러처리

younoah·2022년 1월 29일
2

[nodeJS]

목록 보기
9/15

intro

서버에서 에러를 처리하는것은 중요하다.

서버는 많은 사람들이 요청을 하게 될 텐데 하나의 요청이 잘못되어 서버가 다운되면 안되기 때문이다.

에러처리의 포인트는 아래와 같이 2가지이다.

  • 특정 요청에 대해 의미있는 에러 처리해주기
  • 서버가 문제가 발생해도 정상적으로 동작하기 위해 에러 처리해주기

크게 동기와 비동기 상황에서 각각 어떻게 에러처리를 해야하는지 알아보자.


동기 에러처리

예시코드

import express from 'express';
import fs from 'fs';

const app = express();

app.get('/file', (req, res) => {
  const data = fs.readFileSync('/file.txt');
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

fs.readFileSync 함수는 동기적으로 동작하는 함수이다. 그런데 만약 해당 파일을 읽어오는데 에러가 발생하면 가장 마지막 미들웨어에서 에러를 반환한다.

마지막 미들웨어의 에러처리는 범용적으로 사용되는 에러처리로써 작성되었기 때문에 정확한 에러의 의미를 담지 못한다.

어떤 요청에 대해서 미들웨어가 처리하다 에러가 발생하면 해당 미들웨어에서 에러처리를 해주어야 정확한 의미를 담을수 있다.


import express from 'express';
import fs from 'fs';

const app = express();

app.get('/file1', (req, res) => {
  try {
    const data = fs.readFileSync('/file.txt');
  } catch (error) {
    res.sendStatus(404).send('file not found');;
    // next(err); // 혹은 미들웨어의 마지막 안전망에 던지기
  }
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

위와 같이 동기적으로 동작하는 함수는 try-catch 문을 활용하여 에러 처리를 해줄수 있다.


비동기 에러처리 - case1. 일반 비동기

예시코드

import express from 'express';
import fs from 'fs';

const app = express();

app.get('/file', (req, res) => {
  fs.readFile('/file.txt', (err, data) => {}); // 에러 발생!
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});


app.listen(8080);

fs.readFile 함수는 비동기적으로 동작하는 함수이다. 만약 해당 함수에서 에러가 발생하면 에러를 밖으로 뱉어주지 않기 때문에 외부에서는 에러가 발생했는지 안했는지 알 수가 없어서 에러를 반환하지 않고 계속해서 응답을 대기하는 상태가 될 것이다.

각각의 미들웨어는 동기적으로 연결이 되어있는데 fs.readFile 함수는 에러가 발생하면 콜백함수에게 에러를 반환하기 때문에 콜백함수 내부에서 에러 처리를 해주어야한다.


import express from 'express';
import fs from 'fs';

const app = express();

app.get('/file', (req, res) => {
  fs.readFile('/file.txt', (err, data) => { // 에러 발생!
    if (err) {
      res.sendStatus(404).send('file not found');
      // next(err); // 혹은 미들웨어의 마지막 안전망에 던지기
    }
  });
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});


app.listen(8080);

위와 같이 콜백함수 내부에서 에러처리를 해주어야 올바르게 해당 지점에서 발생한 에러를 처리해줄 수 있다.


비동기 에러처리 - case2. 프로미스 비동기

예시코드

import express from 'express';
import fsAsync from 'fs/promises';

const app = express();

app.get('/file', (req, res) => {
  fsAsync.readFile('/file.txt').then((data) => {...});
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

프로미스를 반환하는 비동기는 어떻게 에러처리를 해야할까?


import express from 'express';
import fsAsync from 'fs/promises';

const app = express();

app.get('/file', (req, res) => {
  fsAsync
    .readFile('/file.txt')
  	.then((data) => {...})
    .catch(error => {
      res.sendStatus(404).send('file not found');
    })
    //.catch(next); 혹은 미들웨어의 마지막 안전망에 던지기
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

간단하게 프로미스의 catch 메서드를 통해서 에러 처리를 하면 된다.


비동기 에러처리 - case3. async-await 프로미스 비동기

예시코드

import express from 'express';
import fsAsync from 'fs/promises';

const app = express();

app.get('/file', async (req, res) => { // 프로미스를 리턴
	const data = await fsAsync.readFile('/file.txt'); // 동기적으로 동작
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

async-await에서는 프로미스를 반환하는 비동기 함수가 동기적으로 동작한다. 하지만 마지막 미들웨어의 에러처리에는 도달하지 못한다.

왜냐하면 async-await 미들웨어 자체가 프로미스를 리턴하기 때문이다. 따라서 async 함수 내부에서가 에러가 발생하는것은 프로미스에서 에러가 발생하는것과 동일하다.


import express from 'express';
import fsAsync from 'fs/promises';

const app = express();

app.get('/file', async (req, res) => {
  try {
    const data = await fsAsync.readFile('/file.txt');
  } catch {
    res.sendStatus(404).send('file not found');
    // next(err); // 혹은 미들웨어의 마지막 안전망에 던지기
  }
});

app.use((error, req, res, next) => { // 에러처리, 최후의 수단
  console.error(error);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

async 내부에서 awit을 사용했으므로 프로미스를 반환하는 비동기함수는 동기적으로 동작한다. 따라서 해당 구문에서 try-catch 를 활용하여 에러처리를 하면된다.


정리

동기 에러처리 : try-catch로 에러처리

일반 비동기 함수 에러처리 : 콜백함수에서 에러처리

프로미스를 반환하는 비동기 함수 에러처리 : 프로미스의 catch를 통해서 에러처리

async-await에서 프로미스 비동기 함수 에러처리: try-catch로 에러처리


그냥 편하게 최후의 안전망으로 넘기기

express v4 기준으로 동기적으로 동작하는 로직과 일반 비동기 로직이있는 미들웨어는 에러가 발생한다면 가장 마지막 미들웨어에서 에러를 감지할 수 있다.

하지만 프로미스로 동작하는 비동기 로직이 있는 미들웨어에서 에러가 발생하면 가장 마지막 미들웨어에서 에러를 감지하지 못한다. 이유는 프로미스를 반환하게 되는데 마지막 에러처리를 하는 미들웨어에서는 프로미스를 인식하지 못하기 때문이다.

import express from 'express';
import fs from 'fs';
import fsAsync from 'fs/promises';

const app = express();

// 동기 -> 최후의 안전망에 도달
app.get('/file1', (req, res) => {
  const data = fs.readFileSync('/file1.txt');
});

// 일반 비동기 -> 최후의 안전망에 도달
app.get('/file2', (req, res) => {
  const data = fs.readFile('/file2.txt');
});

// 프로미스 -> 최후의 안전망에 미도달
app.get('/file3', (req, res) => {
  return fsAsync.readFile('/file3.txt');
});

// async-await: 프로미스를 리턴 -> 최후의 안전망에 미도달
app.get('/file4', async (req, res) => {
  const data = await fsAsync.readFile('/file4.txt');
});

app.use((err, req, res, next) => { // 최후의 안전망
  console.error(err);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

프로미스도 최후의 안전망에 도달하게 하려면 어떻게 할까?


아래 2가지를 이용해서 프로미스도 최후의 안전망에 도달하게 할 수 있다.

  1. Async Middleware 사용하기

  2. express v5 사용하기


1. Async Middleware 사용하기

Async Middleware를 사용하면 프로미스도 가장 마지막 에러 처리 미들웨어에서도 포착할 수 있게된다.

우선 Async Middleware를 설치해준다.

$ npm install async-middleware

이후 최상단에서 import 'express-async-errors'; 를 사용만 해주면 자동으로 체이닝을 걸어준다.

import express from 'express';
import fs from 'fs';
import fsAsync from 'fs/promises';
import 'express-async-errors';
// 혹은 import {} from 'express-async-errors';

const app = express();

// 동기 -> 최후의 안전망에 도달
app.get('/file1', (req, res) => {
  const data = fs.readFileSync('/file1.txt');
});

// 일반 비동기 -> 최후의 안전망에 도달
app.get('/file2', (req, res) => {
  const data = fs.readFile('/file2.txt');
});

// 프로미스 -> 최후의 안전망에 도달!!!
app.get('/file3', (req, res) => {
  return fsAsync.readFile('/file3.txt'); // 다만 꼭 프로미스를 리턴해야한다.
});

// async-await: 프로미스를 리턴 -> 최후의 안전망에 도달!!!
app.get('/file4', async (req, res) => {
  const data = await fsAsync.readFile('/file4.txt');
});

app.use((err, req, res, next) => { // 최후의 안전망
  console.error(err);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);

다만 주의해야할 점은 꼭 프로미스를 반환을 해주어야 올바르게 체이닝이 동작한다.


2. express v5 사용하기

express v5 부터는 미들웨어에서 프로미스를 리턴하면 최후의 안전망에 도달하게 하도록 해준다.

작성 기준으로는 v5는 알파버전이다.

아래 명령어를 입력하면 최신버전의 express v5 알파버전이 설치된다.

$ npm install express@next

이후 프로미스를 리턴하도록 미들웨어를 작성하면 최후의 안전망에 에러가 도달한다.

import express from 'express';
import fs from 'fs';
import fsAsync from 'fs/promises';

const app = express();

// 동기 -> 최후의 안전망에 도달
app.get('/file1', (req, res) => {
  const data = fs.readFileSync('/file1.txt');
});

// 일반 비동기 -> 최후의 안전망에 도달
app.get('/file2', (req, res) => {
  const data = fs.readFile('/file2.txt');
});

// 프로미스 -> 최후의 안전망에 도달!!!
app.get('/file3', (req, res) => {
  return fsAsync.readFile('/file3.txt'); // 다만 꼭 프로미스를 리턴해야한다.
});

// async-await: 프로미스를 리턴 -> 최후의 안전망에 도달!!!
app.get('/file4', async (req, res) => {
  const data = await fsAsync.readFile('/file4.txt');
});

app.use((err, req, res, next) => { // 최후의 안전망
  console.error(err);
  res.status(500).json({ message: 'Something went wrong' });
});

app.listen(8080);
profile
console.log(noah(🍕 , 🍺)); // true

1개의 댓글

comment-user-thumbnail
2023년 3월 30일

감사합니다. 참고했습니다.

답글 달기