유동

[Express] node-express에서 에러 핸들링 하기 본문

node.js/ExpressJS

[Express] node-express에서 에러 핸들링 하기

동 선 2023. 10. 24. 20:27

공식 문서 먼저 보고오자

const errorHandling = (err, req, res, next) => {

}
  • (err, req, res, next) 와 같이 4개의 매개변수를 받는 미들웨어를 바로 errorHandling middleware라고 정의한다.

그리고 아래처럼 프로젝트에 루트 파일의 최하단에 선언해준다.

require("dotenv").config();

const express = require("express");
const app = express();
const cookieParser = require("cookie-parser");
const redisClient = require("../config/database/redis");

// 로그
const morgan = require("morgan");
const logger = require("./module/logger");
const morganFormat = process.env.NODE_ENV !== "production" ? "dev" : combined;
// NOTE: morgan 출력 형태 server.env에서 NODE_ENV 설정 production : 배포 dev : 개발

const accountApi = require("./routes/account");
const authApi = require("./routes/auth");
const uploadApi = require("./routes/upload");
const clubApi = require("./routes/club");
const notificationApi = require("./routes/notification");
const boardApi = require("./routes/board");
const generalApi = require("./routes/general");
const promotionApi = require("./routes/promotion");
const noticeApi = require("./routes/notice");
const searchApi = require("./routes/search");

const errorHandling = require("./middleware/errorHandling");

// connect redis client
redisClient.connect();

// global middleware
app.use(morgan(morganFormat, { stream: logger.stream })); // morgan 로그 설정 
app.use(express.json());
app.use(cookieParser());

// api call middleware
app.use("/account", accountApi);
app.use("/auth", authApi);
app.use("/upload", uploadApi);
app.use("/club", clubApi);
app.use("/notification", notificationApi);
app.use("/board", boardApi);
app.use("/search", searchApi);
app.use("/general", generalApi);
app.use("/notice", noticeApi);
app.use("/promotion", promotionApi);

// error handling mddleware
app.use((err, req, res, next) => {
        const result = {
            message: err.message,
        }
        // 개발환경 전용
        console.error(err);

        // 500 error
        if (!err.status) {
            const serverError = new InternerServerException();
            return res.status(serverError.status).send({message: serverError.message});
        }

        return res.status(err.status).send(result);
    });

module.exports = app;

루트파일에 로직이 들어가는걸 싫어하는 굉장히 싫어하는 성향을 가지고있다면(나) 미들웨어를 따로 빼놓을수도 있다.

/src/middleware/errorHandling.js
const { InternerServerException } = require("../module/customError");

const errorHandling = () => {
    return (err, req, res, next) => {
        const result = {
            message: err.message,
        }
        // 개발환경 전용
        console.error(err);

        // 500 error
        if (!err.status) {
            const serverError = new InternerServerException();
            return res.status(serverError.status).send({message: serverError.message});
        }

        return res.status(err.status).send(result);
    }
}

module.exports = errorHandling;
// error handling middleware
app.use(errorHandling());

현재 개발중인 프로젝트에 동아리 내 모든 일반 게시물을 가져오는 api가 있는데,
이걸 예로 들어보겠다.

해당하는 api에서 발생하는 의미론적 예외는

  1. 동아리에 가입되어 있지 않은 사용자가 해당 api를 호출 시
  2. path로 받은 clubId가 존재하지 않는 동아리일 시
  3. validation예외 시정도 가 있다.

위 나열한 에러들은 400status code와 함께 적절한 메세지를 던져줘야 하는데
나는 일일이 statuscode와 message를 붙여주기 귀찮아서

customError.js파일을 만들고 프로젝트에서 사용하기로 협의된 statuscode 클래스를 미리 정의해줬다.

// customError.js
// 400 error
class BadRequestException extends Error {
    constructor(message) {
        super(message);
        this.name = "BadRequestException";
        this.status = 400;
    }
}

// 401 error
class UnauthorizedException extends Error {
    constructor(message) {
        super(message);
        this.status = 401;
    }
}

// 403 error
class ForbbidenException extends Error {
    constructor(message) {
        super(message);
        this.status = 403;
    }
}

// 500 error
class InternerServerException extends Error {
    constructor() {
        super("서버에서 오류가 발생하였습니다");
        this.status = 500;
    }
}

module.exports = {
    BadRequestException,
    UnauthorizedException,
    ForbbidenException,
    InternerServerException
};
  • 각 클래스마다 기본 Error객체를 상속받아주고 status code와 name을 지어준다
// 동아리 내 모든 일반 게시물을 가져오는 api
// 권한: 해당 동아리에 가입되어있어야 함
router.get("/list/club/:clubId", loginAuth, async (req, res, next) => {
    const userId = req.decoded.id;
    const { clubId } = req.params;
    const result = {
        message: "",
        data: {}
    };
    const page = Number(req.query.page || 1);

    try {
        validate(clubId, "clubId").checkInput().isNumber();
        validate(page, "page").isNumber().isPositive();

        // 권한 체크
        const selectAuthSql = ``;
        const selectAuthParam = [userId, clubId];
        const selectAuthData = await pool.query(selectAuthSql, selectAuthParam);
        // 만약 동아리가 존재하지 않는다면?
        if (selectAuthData.rowCount === 0) {
            throw new BadRequestException("존재하지 않는 동아리입니다");
        }
        // 만약 동아리에 가입하지 않은 사용자라면?
        if (selectAuthData.rows[0].position === null) {
            throw new BadRequestException("동아리에 가입하지 않은 사용자입니다");
        }
        const offset = (page - 1) * POST.MAX_POST_COUNT_PER_PAGE;
        const selectAllPostCountSql = ``;
        const selectAllPostCountParam = [clubId];
        const selectAllPostSql = ``;
        const selectAllPostParam = [clubId, offset, CLUB.MAX_ALL_POST_COUNT_PER_PAGE];
        const selectAllPostCountData = await pool.query(selectAllPostCountSql, selectAllPostCountParam);
        const selectAllPostData = await pool.query(selectAllPostSql, selectAllPostParam);
        result.data = {
            count: selectAllPostCountData.rows[0].count,
            posts: selectAllPostData.rows
        }

    } catch (error) {
        return next(error);
    }
    res.send(result);
});

해당 API에서 의미론적 예외(400에러) 발생 시에는 조건문으로 잡아줘서 미리 지정해둔 400에러를 throw 해준다.

catch에 있는 next에서 error를 받으면 아래의 errorHadnling미들웨어로 알아서 찾아간다

const { InternerServerException } = require("../module/customError");

const errorHandling = () => {
    return (err, req, res, next) => {
        const result = {
            message: err.message,
        }
        // 개발환경 전용
        console.error(err);

        // 500 error
        if (!err.status) {
            const serverError = new InternerServerException("서버에서 오류가 발생하였습니다");
            return res.status(serverError.status).send(serverError.message);
        }

        return res.status(err.status).send(result);
    }
}

module.exports = errorHandling;

여기서 특이한점은 status code를 지정해주지 않은 에러는 전부 500으로 되돌려보냈다.
따라서 DB오류, 백엔드 어플리케이션 오류는 전부 내가 설정한 500에러로 클라이언트에게 전달된다.

  1. customError객체 입맛대로 만들고
  2. try-catch내에서 예외처리하고싶을때 customError throw해서 next(error)로 넘기면
  3. 된다