에러 핸들러와 미들웨어

윤태규·2023년 12월 11일
  1. 미들웨어 (Middleware)
    1) 미들웨어 기본 개념
    미들웨어(Middleware)는 서버의 요청(Request)-응답(Response) 과정에서 중간에 위치하여 특정 기능을 수행하는 함수
    예를 들어, 모든 요청에 대해서 로그(Logging)를 남기거나, 특정 사용자만 API를 접근(Authentication & Authorization)하게 하고 싶을 때도 미들웨어를 사용
    또한 사용자가 웹 페이지에서 Form을 통해 전송한 데이터를 서버에서 쉽게 파싱(Body Parser)하여 사용할 수 있게 해주는 미들웨어도 존재
    2) Express.js에서 미들웨어 작성해보기
    app.use((req, res, next) => {
    // 필요한 코드
    });
  • req: 요청(Request)에 대한 정보가 담겨있는 객체입니다.
    • HTTP Headers, Query Parameters, URL 등 브라우저가 서버로 보내는 정보들이 담겨있습니다.
  • res: 응답(Response)을 위한 기능이 제공됩니다.
    • 어떤 HTTP Status Code로 응답 할지, 어떤 데이터 형식으로 응답 할지, 헤더는 어떤 값을 넣어 응답 할지 다양한 기능을 제공합니다.
  • next: 다음 스택으로 정의된 미들웨어를 호출합니다.
    Request 로그 남기는 미들웨어 작성
    app.use((req, res, next) => {
    console.log('Request URL:', req.originalUrl, ' - ', new Date());
    next();
    });
  • 3) 미들웨어는 어떤 경우에 사용하는게 적합할까요?
    - 미들웨어는 여러분이 만들기 위한 기능에 다양하게 사용 가능하고 또한 관리 측면에서도 많은 이점을 가져다 줍니다.
    - 예를 들어, 인증 미들웨어는 사용자가 로그인 상태인지 확인하고, 로깅 미들웨어는 클라이언트의 요청에 대한 정보를 기록하며, 에러 핸들링 미들웨어는 에러를 처리하게됩니다.
    - 이미 존재하는 미들웨어를 보면 다양하게 존재하고 있습니다.
    4) 여러개의 미들웨어가 겹치는 경우 동작하는 방식
    app.use((req, res, next) => {
    console.log('첫번째 미들웨어');
    next();
    });

app.use((req, res, next) => {
console.log('두번째 미들웨어');
next();
});

app.use((req, res, next) => {
console.log('세번째 미들웨어');
next();
});

// print: 첫번째 미들웨어
// print: 두번째 미들웨어
// print: 세번째 미들웨어

  • 위와같은 그림처럼 순차적으로 미들웨어를 통과하고 중간에 응답을해서 종료가 되거나 다음 미들웨어로 넘어가서 터미널에 첫번째 미들웨어라는 로그부터 차례대로 세번째 미들웨어라는 로그가 쓰여진걸 확인 할 수 있습니다.
  • 하지만 미들웨어를 거치는 중간에 next() 가 실행되지 않으면 다음 미들웨어는 실행되지 않고, 클라이언트의 요청은 거기서 종료됩니다.
  • 현재 미들웨어에서 응답을 보내는 경우, 즉 res.send()이나 res.json()등의 메서드를 호출하는 경우에는 next() 를 호출하면 안됩니다. 이렇게 하지 않으면 이미 요청이 종료된 상태에서 다른 미들웨어가 응답을 보내려고 하여 중복된 요청이 전달되는 문제가 발생하게됩니다.
  • 5) Router와 미들웨어의 차이
    • Router와 미들웨어는 서로 다른 방식처럼 보이지만 Router는 미들웨어 기반으로 구현된 객체이므로 미들웨어와 동일한 방식으로 작동됩니다.
    • 즉, Router는 미들웨어 함수를 특정 경로에 바인딩하는 역할을 하며, 요청이 들어온 URL 경로에 따라 서로 다른 미들웨어를 실행시킬 수 있게 도와줍니다.
  • 6) Express.js의 미들웨어가 실행되는 경우
    • app.use(Middleware) : 모든 요청에서 미들웨어가 실행된다.
    • app.use(’/api’, Middleware) : /api로 시작하는 모든 요청에서 미들웨어를 실행한다.
    • app.post(’/api’, Middleware, (req,res,)=>{} ) : /api로 시작하는 POST 요청에서 미들웨어를 실행한다.
  1. 데이터 유효성 검증 라이브러리 Joi
    1) Joi란?
    📌 JavaScript 유효성 검증을 위한 라이브러리입니다. Joi는 여러 타입과 규칙을 이용해 유효성을 검증할 수 있으며, 유효성 검증에 실패하면 오류를 발생시킵니다.
    2) 유효성 검증(Validation)이란 무엇인가?
    특별한게 아니지만 개발을 하면서 가장 중요한것중 하나입니다.
    Validation은 말 그대로 어떤것을 검증한다고 보면 됩니다.
    function is1(value) {
    return value === 1;
    }
    3) Joi를 이용한 Validation 시작하기
    문자열 길이 검증하기
    import Joi from 'joi';

// Joi 스키마를 정의합니다.
const schema = Joi.object({
// name Key는 문자열 타입이고, 필수로 존재해야합니다.
// 문자열은 최소 3글자, 최대 30글자로 정의합니다.
name: Joi.string().min(3).max(30).required(),
});

// 검증할 데이터를 정의합니다.
const user = { name: 'Foo Bar' };

// schema를 이용해 user 데이터를 검증합니다.
const validation = schema.validate(user);

// 검증 결과값 중 error가 존재한다면 에러 메시지를 출력합니다.
if (validation.error) {
console.log(validation.error.message);
} else {
// 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지를 출력합니다.
console.log('Valid Data!');
}
이메일 검증하기
import Joi from 'joi';

// Joi 스키마를 정의합니다.
const schema = Joi.object({
// name Key는 문자열 타입이고, 필수로 존재해야합니다.
// 문자열은 이메일 형식에 맞아야합니다.
email: Joi.string().email().required(),
});

// 검증할 데이터를 정의합니다.
const user = { email: 'foo@example.com' };

// schema를 이용해 user 데이터를 검증합니다.
const validation = schema.validate(user);

// 검증 결과값 중 error가 존재한다면 에러 메시지를 출력합니다.
if (validation.error) {
console.log(validation.error.message);
} else {
// 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지를 출력합니다.
console.log('Valid Email User!');
}
5) Joi를 이용한 Validation 처리 (비동기)
import Joi from 'joi';

// Joi 스키마를 정의합니다.
const schema = Joi.object({
// name Key는 문자열 타입이고, 필수로 존재해야합니다.
// 문자열은 최소 3글자, 최대 30글자로 정의합니다.
name: Joi.string().min(3).max(30).required(),
});

// 검증할 데이터를 정의합니다.
const user = { name: 'Foo Bar' };

try {
// schema를 이용해 user 데이터를 검증합니다.
const validation = await schema.validateAsync(user);
// 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지를 출력합니다.
console.log('Valid Data!');
} catch (error) {
// 검증에 실패한다면, 에러 메시지를 출력합니다.
console.log(error.message);
}
6) 할 일 생성 API 리팩토링하기
// routes/todos.router.js

import Joi from 'joi';

// 할 일 생성 API의 요청 데이터 검증을 위한 Joi 스키마를 정의합니다.
const createTodoSchema = Joi.object({
value: Joi.string().min(1).max(50).required(),
});

router.post('/todos', async (req, res) => {
// 클라이언트에게 전달받은 데이터를 검증합니다.
const validation = await createTodoSchema.validateAsync(req.body);

// 클라이언트에게 전달받은 value 데이터를 변수에 저장합니다.
const { value } = validation;

// Todo모델을 사용해, MongoDB에서 'order' 값이 가장 높은 '해야할 일'을 찾습니다.
const todoMaxOrder = await Todo.findOne().sort('-order').exec();

// 'order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.
const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

// Todo모델을 이용해, 새로운 '해야할 일'을 생성합니다.
const todo = new Todo({ value, order });

// 생성한 '해야할 일'을 MongoDB에 저장합니다.
await todo.save();

res.status(201).json({ todo });
});
7) try/catch 설정하기
// /routes/todos.router.js

/ 할 일 생성 API 리팩토링, 에러 처리 /
router.post('/todos', async (req, res) => {
try {
// 클라이언트에게 전달받은 데이터를 검증합니다.
const validateBody = await createTodoSchema.validateAsync(req.body);

// 클라이언트에게 전달받은 value 데이터를 변수에 저장합니다.
const { value } = validateBody;

// Todo모델을 사용해, MongoDB에서 'order' 값이 가장 높은 '해야할 일'을 찾습니다.
const todoMaxOrder = await Todo.findOne().sort('-order').exec();

// 'order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.
const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

// Todo모델을 이용해, 새로운 '해야할 일'을 생성합니다.
const todo = new Todo({ value, order });

// 생성한 '해야할 일'을 MongoDB에 저장합니다.
await todo.save();

return res.status(201).json({ todo });

} catch (error) {
console.error(error);
// Joi 검증에서 에러가 발생하면, 클라이언트에게 에러 메시지를 전달합니다.
if (error.name === 'ValidationError') {
return res.status(400).json({ errorMessage: error.message });
}

// 그 외의 에러가 발생하면, 서버 에러로 처리합니다.
return res
  .status(500)
  .json({ errorMessage: '서버에서 에러가 발생하였습니다.' });

}
});

  1. 에러 처리 미들웨어
  • 1) 에러 처리는 왜 필요할까요?
    에러원하지 않았던 비즈니스 로직이 수행되지 않도록 하기 위해 사용합니다.
    만약, 중복되어선 안되는 데이터에 다시 등록 요청이 들어오게 된다면, 데이터를 삽입하지 않아야하겠죠. 이렇게, 더이상 코드를 실행하지 않아야하는 경우에 에러를 발생시키게 됩니다.
    이런 방식으로, 에러는 우리가 예상치 못한 문제를 미리 방지하도록하여, 서버 코드가 더욱 안정적으로 구성할 수 있는 효과를 얻게됩니다.
    2) Express.js의 에러 처리 미들웨어
    app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
    });
    3) 에러 발생 시 에러를 처리하는 미들웨어 작성하기
    // /middlewares/error-handler.middleware.js

export default function (err, req, res, next) {
console.error(err);

// Joi 검증에서 에러가 발생하면, 클라이언트에게 에러 메시지를 전달합니다.
if (err.name === 'ValidationError') {
return res.status(400).json({ errorMessage: err.message });
}

// 그 외의 에러가 발생하면, 서버 에러로 처리합니다.
return res
.status(500)
.json({ errorMessage: '서버에서 에러가 발생하였습니다.' });
}
4) 에러 처리 미들웨어 등록하기
// app.js

import ErrorHandlerMiddleware from './middlewares/error-handler.middleware.js';

...

// /api 주소로 접근하였을 때, router와 TodosRouter로 클라이언트의 요청이 전달됩니다.
app.use('/api', [router, TodosRouter]);

// 에러 핸들링 미들웨어를 등록합니다.
app.use(ErrorHandlerMiddleware);

  • 에러 처리 미들웨어는 왜 Router 하단에 등록하는건가요?
    <aside>
    💡 미들웨어는 **등록된 순서대로 실행**됩니다. **TodoRouter**에서 비즈니스 로직을 수행한 후 발생한 에러는 **다음 미들웨어로 전달**됩니다. 이 때, 에러 처리 미들웨어가 라우터 이후에 등록되어 있으면, 에러를 잡아 처리할 수 있게 되는것이죠.
    반대로, **라우터 이전에 에러 처리 미들웨어를 등록**하면 라우터에서 발생한 에러를 처리할 수 없습니다.
    왜냐하면, **라우터에서 발생한 에러는 라우터 이후에 등록된 미들웨어로 전달**되기 때문입니다. 이렇게 되면 에러 처리 미들웨어가 에러를 잡지 못하게 되는것이죠. 따라서, **항상 에러 처리 미들웨어는 라우터 설정 코드 하단**에 위치해야 합니다.
    5) 할 일 등록 API 리팩토링
    // /routes/todos.router.js

/ 에러 핸들러 /
router.post('/todos', async (req, res, next) => {
try {
// 클라이언트에게 전달받은 데이터를 검증합니다.
const validateBody = await createTodoSchema.validateAsync(req.body);

// 클라이언트에게 전달받은 value 데이터를 변수에 저장합니다.
const { value } = validateBody;

// Todo모델을 사용해, MongoDB에서 'order' 값이 가장 높은 '해야할 일'을 찾습니다.
const todoMaxOrder = await Todo.findOne().sort('-order').exec();

// 'order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.
const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

// Todo모델을 이용해, 새로운 '해야할 일'을 생성합니다.
const todo = new Todo({ value, order });

// 생성한 '해야할 일'을 MongoDB에 저장합니다.
await todo.save();

return res.status(201).json({ todo });

} catch (error) {
// 발생한 에러를 다음 에러 처리 미들웨어로 전달합니다.
next(error);
}
});
6) 폴더 구조 살펴보기
todo-list
├── app.js
├── assets
│   ├── asset-manifest.json
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   ├── robots.txt
│   └── static
├── middlewares
│   └── error-handler.middleware.js
├── package.json
├── routes
│   └── todos.router.js
├── schemas
│   ├── index.js
│   └── todo.schema.js
├── todo-list-static-files.zip
└── yarn.lock

  • app.js
    • 전체 어플리케이션의 시작점입니다.
    • 미들웨어(Middleware)라우터(Router)를 등록하며, 서버를 시작하는 역할을 담당합니다.
  • middlewares
    • 미들웨어를 정의하기 위해 사용합니다.
    • 에러 핸들러, 로깅, 사용자 인증과 같은 미들웨어를 이 폴더에서 관리합니다.
  • routes
    • Express.js의 라우터(Router)를 관리하기 위해 사용합니다.
    • API 경로를 정의하며, 해당 경로에서 실행될 함수를 관리하는 역할을 담당합니다.
  • schemas
    • MongoDB를 사용하기 위한 mongoose스키마(Schema)모델(Model)을 정의하기 위해 사용합니다.
    • MongoDB 데이터의 구조데이터를 처리할 메서드를 정의하는 역할을 담당합니다.
  • assets
    • 프론트엔드 파일을 서빙하기 위해 사용하는 폴더입니다.
    • 웹페이지를 구성하는 HTML, CSS, JavaScript 파일, 이미지 등 여러 파일들이 이 폴더에 위치하게 됩니다.
profile
끝까지 가자

0개의 댓글