본문 바로가기
내일배움 강의/강의- Node.js 입문, 숙련

입문 2주차 8 에러 핸들러와 미들웨어, Joi 설치

by GREEN나무 2024. 11. 24.
728x90

<Goal>
1. Express.js의 미들웨어에 대한 개념을 알아봅니다.
2. 데이터 유효성 검증 라이브러리 Joi에 대해 알아봅니다.
3. Express.js의 에러 처리 미들웨어를 알아보고 구현해봅니다.

 


01. 미들웨어 (Middleware)


1) 미들웨어 기본 개념

◆ 미들웨어란?

웹 서버에서 요청을 받을때, 모든 요청에 대한 공통적인 처리를 하고싶을때 필요한 것이 바로 미들웨어(Middleware)입니다.
미들웨어(Middleware)는 서버의 요청(Request)-응답(Response) 과정에서 중간에 위치하여 특정 기능을 수행하는 함수라고 볼 수 있습니다.
   ex 1) 모든 요청에 대해서 로그(Logging)를 남기거나, 특정 사용자만 API를 접근(Authentication & Authorization)하게 하고 싶을 때 사용하는 미들웨어. 
   ex 2)   사용자가 웹 페이지에서 Form을 통해 전송한 데이터를 서버에서 쉽게 파싱(Body Parser)하여 사용할 수 있게 해주는 미들웨어.

 Express.js의 미들웨어

이전 주차에서 Express.js를 소개할 때, 그 특징 중 하나로 미들웨어의 지원을 언급했었습니다. 미들웨어는 Express.js의 핵심 기능 중 하나로, 다양한 기능을 제공합니다.
   ex) Body Parser는 클라이언트의 요청(Request) 본문 데이터인 body를 쉽게 파싱할 수 있게 해주는 미들웨어입니다. 

// URL-encoded 데이터와 JSON 데이터를 파싱하여 req.body에 저장하는 미들웨어 설정

//  폼데이터에 대한 바디파스 역할을 작성 (application/x-www-form-urlencoded 타입의 데이터를 파싱)
app.use(express.urlencoded({ extended: false }));

// JSON 데이터를 처리 (application/json 타입의 데이터를 파싱)
// req.body에 클라이언트가 전달하는 리퀘스트 데이터를 넣어주는 것 처럼 사용할 수 있습니다.
app.use(express.json());

app.use() : 전역 미들웨어를 등록하는 방법
urlencoded: form-urlencoded 라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어에요!
json: JSON 이라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어에요!

 

2) Express.js에서 미들웨어 작성해보기

 미들웨어는 아래의 인터페이스로 작성할 수 있습니다.

app.use((req, res, next) => {
  // 필요한 코드
});

위의 형태에서 req, res, next는 각자 역할을 지니고 있는 인자(Parameter)입니다.
req : 요청(Request)에 대한 정보가 담겨있는 객체입니다.
   HTTP Headers, Query Parameters, URL 등 브라우저가 서버로 보내는 정보들이 담겨있습니다.
res : 응답(Response)을 위한 기능이 제공됩니다.
   어떤 HTTP Status Code로 응답 할지(어떤 형태로 응답할 지), 어떤 데이터 형식으로 응답 할지, 헤더는 어떤 값을 넣어 응답 할지 다양한 기능을 제공합니다.
next : 호출되었을 때 다음 스택으로 정의된 미들웨어를 호출합니다.

 

모든 Request 를 로그 남기는 미들웨어 작성

미들웨어는 위에서 부터 아래로 순차적으로 실행됩니다.

req로그 미들웨어는 실행하는 함수 위에 위치해야 합니다.

app.use((req, res, next) => {
    console.log('Request URL:', req.originalUrl, ' - ', new Date());
    next();
});

 

클라이언트의 요청사항이 ' 'Request URL: 요청한 경로 -  요청 시간' 형태로 콘솔로그에 남고 다음 미들웨어가 실됩니다.

// app.js

import express from "express";
import connect from "./schemas/index.js";
import todosRouter from "./routes/todos.router.js";

const app = express();
const PORT = 3000;

connect();

// Express에서 req.body에 접근하여 body 데이터를 사용할 수 있도록 설정합니다.
app.use(express.json()); // 미들웨어 1
app.use(express.urlencoded({ extended: true })); // 미들웨어 2

// static Middleware, express.static()을 사용하여 정적 파일을 제공합니다.
// ./assets폴더를 바탕으로 서빙을 할 것입니다.
app.use(express.static("./assets"));  // 미들웨어 3

// 미들웨어 4
// 모든 요청을 로그로 남기기
app.use((req, res, next) => {   // 미들웨어 4
  console.log("Request URL:", req.originalUrl, " - ", new Date());
  next();
});

const router = express.Router();

router.get("/", (req, res) => {
  return res.json({ message: "Hi!" });
});

// 미들웨어 5
app.use("/api", [router, todosRouter]);

app.listen(PORT, () => {
  console.log(PORT, "포트로 서버가 열렸어요!");
});

코드 저장 후 node app.js로 서버를 재실행하세요. insomnia로 아무거나 실행 한 뒤 터미널을 확인해보면 추가된 로그를 확인 할 수 있습니다. 

3) 미들웨어는 어떤 경우에 사용하는게 적합할까요?

미들웨어는 여러분이 만들기 위한 기능에 다양하게 사용 가능하고 또한 관리 측면에서도 많은 이점을 가져다 줍니다.
예를 들어, 인증 미들웨어는 사용자가 로그인 상태인지 확인하고, 로깅 미들웨어는 클라이언트의 요청에 대한 정보를 기록하며, 에러 핸들링 미들웨어는 에러를 처리하게됩니다.
이미 존재하는 미들웨어를 보면 다양하게 존재하고 있습니다.
기본 미들웨어: https://expressjs.com/ko/4x/api.html

 

Express 4.x - API 참조

Access the API reference for Express.js 4.x, detailing all modules, methods, and properties for building web applications with this version.

expressjs.com

 

 

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() 가 실행되지 않으면 다음 미들웨어는 실행되지 않고, 클라이언트의 요청은 거기서 종료됩니다. 

▶ 만약 next()를 실행시키지 않고 클라이언트의 req도 전달하지 않게 되면 무한정 대기하는 상태가 됩니다.

▶ 그러니 미들웨어를 사용할 떄 꼭 next()로 다음 미들웨어를 실행시키거나 클라이언트에게 respons를  return해야 합니다.
▶ 현재 미들웨어에서 응답을 보내는 경우, 즉 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 요청에서 미들웨어를 실행한다.

   Middleware를 넣어주면 그 미들웨어를 거친 다음에 오른쪽에 있는 실제 함수가 실행됩니다.

 

7) 미들웨어 문제 풀이

요구사항
1. 클라이언트는 GET localhost:3000/ API를 호출합니다.
2. 서버의 Terminal에 출력되는 결과를 예상해주세요.
3. 클라이언트의 응답(Response)에 출력되는 결과를 예상해주세요.


예시 코드

// app.js

import express from "express";

const app = express();
const PORT = 3000;

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

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

app.get("/", (req, res, next) => {
  console.log("GET / 요청이 발생했습니다.");
  next();
});

app.use((req, res, next) => {
  console.log("세번째 미들웨어");
  res.json({ message: "Hi" });
});

app.use((req, res, next) => {
  console.log("네번째 미들웨어");
  res.json({ message: "마지막 미들웨어 입니다." });
});

app.listen(PORT, () => {
  console.log(PORT, "포트로 서버가 열렸어요!");
});

/* 로컬 호스트
{
	"message": "Hi"
}
    */

/*console.log 출력
3000 포트로 서버가 열렸어요!
첫번째 미들웨어
두번째 미들웨어
세번째 미들웨어
*/


​미들웨어는 순차적으로 실행됩니다.
next() 메서드를 실행하지 않을 경우 다음 미들웨어로 넘어가지 않습니다.
“세번째 미들웨어”에서는 next() 메서드를 요청하지 않고, res.json() 메서드를 실행하였기 때문에, “네번째 미들웨어”로 넘어가지 않고, API가 종료된 것입니다.
만약, res.json() 또는 next() 메서드 모두 호출되지 않는다면, 해당 요청이 무한정 대기할 수 있으니 이점 주하세요​

// localhost:3000/api
{
  "message": "Hi"
}


일반적으로 GET / Router가 아니라, “세번째 미들웨어”에서 res.json() 메서드를 실행하였기 때문에, 해당하는 응답이 발생하였습니다.
만약, “세번째 미들웨어”에서 next() 메서드가 실행되어, “네번째 미들웨어”로 넘어가게 되었다면, 응답(Response)를 2번 전달하게되어, 아래와 같은 에러 메시지가 출력되었을 것입니다.

 

 

02. 데이터 유효성 검증 라이브러리 Joi

1) Joi란?

Joi는 JavaScript 유효성 검증을 위한 라이브러리입니다. Joi는 여러 타입과 규칙을 이용해 유효성을 검증할 수 있으며, 유효성 검증에 실패하면 오류를 발생시킵니다.
   ex) 일반적으로 어플리케이션의 회원가입 기능은 각각의 사이트에서는 아이디, 이메일, 비밀번호 등의 형식을 정해놓고 있습니다. 매번 회원가입을 할 때 사이트마다 비밀번호 패턴이 달라서 불편했던 경우가 있을 겁니다.
이러한 것처럼 클라이언트가 요청한 정보들이 서버로 전달될 때, 알맞은 형식인지 아닌지 검증하는 작업이 필요하게됩니다.  Joi를 사용하게 된다면 이런 유효성 검증 작업을 효과적으로 처리할 수 있게 됩니다.

 

2) 유효성 검증(Validation)이란 무엇인가?

Validation은 말 그대로 어떤것을 검증한다고 보면 됩니다. 개발을 하면서 가장 중요한것중 하나입니다.

function is1(value) {
	return value === 1;
}

위 코드는 단순히 값이 1인지 아닌지 판단해서 Boolean 타입의 값을 반환하는 함수입니다.
이렇게 단순한 함수조차 Validation. 즉, 검증을 위한 코드가 됩니다.
여러분은 날이 갈수록 당연히 더 복잡하고 어려운 검증 로직을 짜게 될텐데요, 이런 데이터를 검증하는 것을 더 쉽고 간결하게 작성하도록 도와주는 joi라는 라이브러리를 사용해볼 예정입니다 
Nest.js 프레임워크에서는 Pipe라는 이름으로 불리기도 한답니다. 

 

3) Joi 설치하기

Joi 패키지를 설치하기
# yarn을 이용해 Joi를 설치합니다.

yarn add joi


​package.json에 "joi": "^17.13.3",가 추가됬니다.

 

4) Joi를 이용한 Validation 시작하기

Joi를 이용한 간단한 유효성 검사.

// 1. import Joi
import Joi from "joi";

// 2. Joi 스키마를 정의합니다.
const schema = Joi.object({ // 정의
});

// 3. 검증할 데이터를 정의합니다.
// 4. schema를 이용해 데이터를 검증합니다.
// 5. 검증 결과값 중 error가 존재한다면 에러 메시지를 출력합니다.
// 6. 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지를 출력합니다.


◆ 문자열 길이 검증하기

// 문자열 길이 검증하기
import Joi from 'joi';

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

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

// schema를 이용해 user 데이터를 검증합니다.
// validation애 검증 결과를 할당합니다.
// 정의한Joi 스키마.validate(검증할 데이터)
const validation = schema.validate(user);

// 검증 결과값 중 error가 존재한다면 에러 메시지를 출력합니다.
if (validation.error) { // 검증중 오류가 나면 '검사용변수.error'가 생성됩니다.
  console.log(validation.error.message);
} else {
  // 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지를 출력합니다.
  console.log('Valid Data!');
}


예제 코드를 실행하면, name 속성이 문자열인지 검증하고, 3~30글자 사이의 문자열인지 검증하게 됩니다.
만약, 유효하다면 “Valid Data!”라는 메시지를 출력합니다. 만약, name 속성이 문자열이 아니라면, 에러 정보를 출력하게됩니다.


◆ 이메일 검증하기

import Joi from "joi";

// Joi 스키마를 정의합니다.
const schema = Joi.object({
  // email Key는 Joi.string()을 통해 문자열 형식을 가져야 합니다.
  // .email() : joi에서 작성하는 이메일의 패턴을 사용할 수 있습니다.
  // Joi.string().email() : 해당 키가 실제 이메일 형식이어야 한다.
  // .required() : 해당 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!");
}


 email 속성이 문자열이고, 이메일 형식인지 검증하게 됩니다.
만약, 유효하다면 “Valid Email User!”라는 메시지를 출력합니다. 만약, email 속성이 문자열이 아니거나, 이메일 형식에 일치하지 않다면, 에러 메시지가 출력됩니다.


상세한 Joi 문법 : https://joi.dev/api/?v=17.13.3

 

5) Joi를 이용한 Validation 처리 (비동기)

Joi는 검증한 결과값에 에러 내용이 포함되도록 구현할 수 있지만, 검증에 실패했을 때, 에러가 발생 (+로직 종료) 하도록 구현하는 방법 또한 존재합니다. 이 방법은 비동기적으로 처리하는 방법입니다.


◆ 비동기로 문자열 길이 검증하기

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); // 에러 발생시 로직종료(되는걸 catch에서 잡음)
  // 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지를 출력합니다.
  console.log("Valid Data!");
} catch (error) {
  // 검증에 실패한다면, 에러 메시지를 출력합니다.
  console.log(error.message);
}


예제 코드에서 유효성 검증을 하기 위해 사용하는 메서드를 validate() → validateAsync()로 변경하여 데이터를 비동기적으로 검증합니다.
비동기로 Joi를 사용하게 된다면, 이전과 다르게 반환되는 결과값에 에러 메시지가 포함되지 않고, 바로 에러가 발생하게 됩니다. 에러가 발생하여 서버가 종료되는 것을 방지하기 위해 try/catch 구문을 사용하였습니다.

 

import Joi from "joi";

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

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

const validation = schema.validate(user);
console.log(validation);


Joi를 이용한 데이터 유효성 검사 수행 유형

   1. 검사에 실패하면 에러 메시지를 반환

   2. 하거나, 에러를 발생시키기


6) 할 일 생성 API 리팩토링하기

할 일 생성 API를 리팩토링하여, 유효성 검증 기능을 추가하
이전까지 할 일 생성 API는 클라이언트에게 전달받은 value데이터가 존재하지 않을 때에만, 에러 메시지를 전달했습니다. 이번에는 value데이터에 상세한 요구사항을 정의하고, 이에 따른 유효성 검증을 진행해보도록 하겠습니다.

◆ 할 일 생성 API 유효성 검사 요구사항

1. value 데이터는 필수적으로 존재해야한다.
2. value 데이터는 문자열 타입이어야한다.
3. value 데이터는 최소 1글자 이상이어야한다.
4. value 데이터는 최대 50글자 이하여야한다.
5. 유효성 검사에 실패했을 때, 에러가 발생해야한다.

 

할 일 생성 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 });
});

 

할일 등록시 유효성 검사

import Joi from "joi"; // joi import
...

// 할 일 생성 API의 요청 데이터 검증을 위한 Joi 스키마를 정의합니다.
// createTodoSchema는 Joi.object로 value를 검증합니다.
// value는 문자열 타입, 최소 1글자 이상 최대 50글자 이하, 데이터가 필수적으로 존재해야한다.
const createTodoSchema = Joi.object({
  value: Joi.string().min(1).max(50).required(),
});

/** 할일등록 API **/
router.post("/todos", async (req, res, next) => {
  // // 1. 클라이언트에게 전달받은 value 데이터를 변수에 저장합니다.
  // const { value } = req.body;

  // 클라이언트에게 전달받은 데이터를 검증합니다.
  // validateAsync : 애러 발생시키기 위해서 비동기메소드로 검증
  const validation = await createTodoSchema.validateAsync(req.body);
  // 검증 성공
  const { value } = validation;
  ...
더보기
// /routes/todos.router.js

import express from "express";
import Joi from "joi"; // joi import
import Todo from "../schemas/todo.schema.js";

// 라우터 생성
const router = express.Router();

/**할 일 생성 API 유효성 검사 요구사항
value 데이터는 필수적으로 존재해야한다.
value 데이터는 문자열 타입이어야한다.
value 데이터는 최소 1글자 이상이어야한다.
value 데이터는 최대 50글자 이하여야한다.
유효성 검사에 실패했을 때, 에러가 발생해야한다. = validateAsync로 검증하세요 */

// 할 일 생성 API의 요청 데이터 검증을 위한 Joi 스키마를 정의합니다.
// createTodoSchema는 Joi.object로 value를 검증합니다.
// value는 문자열 타입, 최소 1글자 이상 최대 50글자 이하, 데이터가 필수적으로 존재해야한다.
const createTodoSchema = Joi.object({
  value: Joi.string().min(1).max(50).required(),
});

/** 할일등록 API **/
router.post("/todos", async (req, res, next) => {
  // // 1. 클라이언트에게 전달받은 value 데이터를 변수에 저장합니다.
  // const { value } = req.body;

  // 클라이언트에게 전달받은 데이터를 검증합니다.
  // validateAsync : 애러 발생시키기 위해서 비동기메소드로 검증
  const validation = await createTodoSchema.validateAsync(req.body);
  // 검증 성공
  const { value } = validation;

  // 1-5. 데이터 유효성 검사 기능 추가
  if (!value) {
    return res
      .status(400) // 400 : 클라이언트 잘못으로 오류 발생
      .json({ errorMessage: "해야할 일 데이터가 존재하지 않습니다." });
  }

  // 2. 해당하는 마지막 order 데이터를 조회 (MongoDB에서 'order' 값이 가장 높은 '해야할 일')
  const todoMaxOrder = await Todo.findOne().sort("-order").exec();

  // 3. 만약 존재한다면 현재 할 일에 +1, order 데이터가 존재하지 않는다면 1로 할당합니다.('order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.)
  const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

  // 4. 해야할 일 등록
  // todo를 실제 인스턴스형식으로 만듦
  const todo = new Todo({ value, order });
  // 실제로 DB에 저장
  await todo.save();

  // 5. 해야할 일을 클라이언트에게 반환
  return res.status(201).json({ todo });
});

/** 해야할 일 목록 조회 API**/
// API등록은 router에서 합니다.
router.get("/todos", async (req, res, next) => {
  // 1. 해야할 일 목록 조회를 진행한다.
  const todos = await Todo.find().sort("-order").exec();

  // 2. 해야할 일 목록 조회 결과를 클라이언트에게 반환 한다.
  return res.status(200).json({ todos }); // 200 성공
});

/** 순서 변경, 할 일 완료/해제, 할 일 내용 변경 **/
// /:todoId' : 수정해야 할 데이터를 알기 위해 지정. 경로 매개변수
router.patch("/todos/:todoId", async (req, res) => {
  const { todoId } = req.params;

  // 완료여부확인을 위해 done 추가
  const { order, done, value } = req.body;

  const currentTodo = await Todo.findById(todoId).exec();
  if (!currentTodo) {
    return res
      .status(404) // 400은 클라이언트 잘못
      .json({ errorMessage: "존재하지 않는 해야할 일 입니다." });
  }

  // 해야할 일 순서 변경
  if (order) {
    const targetTodo = await Todo.findOne({ order }).exec(); // {order}는  {order:order}를 의미함. 객체의 값이라 {}사용
    if (targetTodo) {
      targetTodo.order = currentTodo.order; // order값 바꾸기.. 4->1이면 2,3번은 그대로 있고 4, 1번만 바뀜
      await targetTodo.save();
    }
    // 변경하려는 '해야할 일'의 order 값을 변경합니니다.
    currentTodo.order = order;
  }

  // 할일을 완료한 경우
  if (done !== undefined) {
    // false는 done에 값이 들어가지 않습니다.
    // 변경하려는 '해야할 일'의 doneAt 값을 변경합니다.
    // 위에서 todoId 조회를 위해 사용한 currentTodo필드에 doneAt라는 컬럼에 할당한다
    // 삼항문으로 done의 값이 있는 경우 그 값을 할당 합니다.
    currentTodo.doneAt = done ? new Date() : null;
  }

  /** 해야할 일 수정 */
  if (value) {
    // 변경하려는 '해야할 일'의 내용을 변경합니다.
    currentTodo.value = value;
  }

  // 변경된 '해야할 일'을 저장합니다.
  await currentTodo.save();

  return res.status(200).json({});
});

/** 할 일 삭제 **/
router.delete("/todos/:todoId", async (req, res) => {
  // 삭제할 '해야할 일'의 ID 값을 가져옵니다.
  // todoId : 경로 매개변수인 req.params에서 가져오기
  const { todoId } = req.params;

  // 삭제하려는 '해야할 일'을 가져옵니다.(조회)
  const todo = await Todo.findById(todoId).exec();
  //만약, 해당 ID값을 가진 '해야할 일'이 없다면 에러를 발생시킵니다.
  if (!todo) {
    return res
      .status(404) // 클라이언트 잘못. id 입력 오류
      .json({ errorMessage: "존재하지 않는 todo 데이터입니다." });
  }

  // 조회된 '해야할 일'을 삭제합니다.
  // MongDB의 데이터 id는 '_id'에 있습니다.
  // 코드에서 중복없이 발급되는 데이터 id를 컬럼todoId에 할당하고 있습니다.
  await Todo.deleteOne({ _id: todoId }).exec();

  return res.status(200).json({});
});

export default router;

 

 

test

1. 입력값이 없을 때 : 오류 발생(로직 종료)

[Error [ValidationError]: "value" is required]

값이 없을 때
값이 없을 때

2. value의 값을 숫자로 넣었을 때 : 에러 발생

[Error [ValidationError]: "value" must be a string]

value의 값이 숫자
value의 값이 숫자

3. value 값이 빈 문자열("") : 에러 발생

[Error [ValidationError]: "value" is not allowed to be empty]

value 값이 빈 문자열("")
value 값이 빈 문자열("")

4. value 값이 50문자 이상의 문자열 일때 : 에러발생

[Error [ValidationError]: "value" length must be less than or equal to 50 characters long]

value 값이 50문자 이상의 문자열
value 값이 50문자 이상의 문자열

5. 정상작동 : 여전히 서버 프로그램 실행중.



Joi.string().min(1).max(50).required()를 사용하여, Joi 스키마가 요구사항에 일치하도록 작성하였습니다. 만약, 클라이언트로부터 전달받은 value데이터가 이 요구사항을 만족하지 않으면, Joi는 에러를 발생시킵니다.
클라이언트에게 전달받은 Body 데이터를 모두 검증하도록 코드를 구현하였습니다. 이렇게 코드를 구현하게 된다면, 여러분들이 단순히 value 데이터 뿐만 아니라, 다른 데이터가 추가되더라도, 하나의 createTodoSchema에서 관리할 수 있어 더욱 효율적으로 코드를 구성할 수 있게 됩니다.

 

7) try/catch 설정하기

Joi의 데이터 유효성 검증을 실패한 코드에서 에러가 발생하는 경우에 대한 대응 방법.
가장 먼저, 서버에서 에러가 발생하게 된다면 클라이언트가 요청(Request)한 처리는 중단되며, 이때 서버는 클라이언트에게 응답(Response)를 보내지 못하게됩니다. 이를 방지하기 위해서는 발생 가능한 에러를 미리 대비하는 예외 처리가 필요합니다.
try/catch문은 이러한 예외 처리를 수행하는 역할을 하게됩니다. try 블록에서는 에러가 발생하면, 실행은 즉시 중단되고 catch 블록이 실행되게됩니다.


 try/catch문을 이용해 에러가 발생하였을 때 에러 정보를 console.log를 통해 출력하고, 클라이언트에게는 현재 발생한 에러메시지와 해당하는 HTTP Status를 전달하는 코드를 작성하기.
할 일 생성 API try/catch

// /routes/todos.router.js
router.post("/todos", async (req, res, next) => {
  try { ////// try
    // 클라이언트에게 전달받은 데이터를 검증합니다.
    // validateAsync : 애러 발생시키기 위해서 비동기메소드로 검증
    const validation = await createTodoSchema.validateAsync(req.body);
    // 검증 성공
    const { value } = validation;

    // 1-5. 데이터 유효성 검사 기능 추가
    if (!value) {
      return res
        .status(400) // 400 : 클라이언트 잘못으로 오류 발생
        .json({ errorMessage: "해야할 일 데이터가 존재하지 않습니다." });
    }

    // 2. 해당하는 마지막 order 데이터를 조회 (MongoDB에서 'order' 값이 가장 높은 '해야할 일')
    const todoMaxOrder = await Todo.findOne().sort("-order").exec();

    // 3. 만약 존재한다면 현재 할 일에 +1, order 데이터가 존재하지 않는다면 1로 할당합니다.('order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.)
    const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

    // 4. 해야할 일 등록
    const todo = new Todo({ value, order });
    await todo.save();

    // 5. 해야할 일을 클라이언트에게 반환
    return res.status(201).json({ todo });
    
    ///////////catch
  } catch (error) {
    console.log(error); // error를 그내로 출력
    // 테스트 결과 에러 메시지 : [Error [ValidationError]: "value" length must be less than or equal to 50 characters long]
    if (error.name === "ValidationError") {
      return res.status(400).json({ errorMessage: error.message });
    }

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


​try/catch 문에서, try 블록은 할 일을 생성하는 비즈니스 로직을 실행하고, catch 블록은 에러를 처리하도록 구현되어 있습니다.
만약, Joi가 발생시킨 에러인 경우, 발생한 에러 메시지를 클라이언트에게 전달하게됩니다. 그 외에 MongoDB를 사용하면서 발생한 에러 또는 비즈니스 로직을 수행하던 중 발생한 에러는 서버 에러라는 메시지를 클라이언트에게 전달하게 됩니다.

 

test

req X
value 값으로 1개 미만의 문자
value 값이 숫자
value 값이 50개 이상의 문자를 받음
정상작동

 

try/catch문을 사용하여 에러 발생시에도 서버가 중단되지 않았습니다.

\todolist> node app.js
3000 포트로 서버가 열렸어요!
MongoDB 연결에 성공하였습니다.
Request URL: /api/todos  -  2024-11-24T13:51:01.060Z
[Error [ValidationError]: "value" is required] {
  _original: {},
  details: [
    {
      message: '"value" is required',
      path: [Array],
      type: 'any.required',
      context: [Object]
    }
  ]
}
Request URL: /api/todos  -  2024-11-24T13:52:28.712Z
[Error [ValidationError]: "value" must be a string] {
  _original: { value: 1 },
  details: [
    {
Request URL: /api/todos  -  2024-11-24T13:53:19.208Z
Request URL: /api/todos  -  2024-11-24T13:53:19.208Z
[Error [ValidationError]: "value" is not allowed to be empty] {
  _original: { value: '' },
  details: [
    {
      message: '"value" is not allowed to be empty',
      path: [Array],
      type: 'string.empty',
      context: [Object]
    }
  ]
}
Request URL: /api/todos  -  2024-11-24T13:54:33.025Z
[Error [ValidationError]: "value" length must be less than or equal to 50 characters long] {
  _original: {
    value: '스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르타스파르 타스파르타스파르타스파르타스파르타스파르타'
  },
  details: [
    {
      message: '"value" length must be less than or equal to 50 characters long',
      path: [Array],
      type: 'string.max',
      context: [Object]
    }
  ]
}
Request URL: /api/todos  -  2024-11-24T13:56:03.028Z

 


03.  에러 처리 미들웨어 

1) 에러 처리는 왜 필요할까요?

에러는 (다음에 오는)원하지 않았던 비즈니스 로직이 수행되지 않도록 하기 위해 사용합니다. 
만약, 중복되어선 안되는 데이터에 다시 등록 요청이 들어오게 된다면, 데이터를 삽입하지 않아야하겠죠. 이렇게, 더이상 코드를 실행하지 않아야하는 경우에 에러를 발생시키게 됩니다.
이런 방식으로, 에러는 우리가 예상치 못한 문제를 미리 방지하도록하여, 서버 코드가 더욱 안정적으로 구성할 수 있는 효과를 얻게됩니다. 

 

2) Express.js의 에러 처리 미들웨어

에러 처리 미들웨어는 Express.js가 공식적으로 제공하는 기능으로 에러를 총괄적으로 제어할 수 있도록 도와줍니다.

◆ 에러 처리 미들웨어 구조

app.use((err, req, res, next) => { // 4개의 인자값을 가지고 있는 미들웨어는 에러처리 미들웨어다.
  console.error(err.stack);
  res.status(500).send('Something broke!');
});


​에러 처리 미들웨어에서 err, req, res, next는 각각 에러, 요청, 응답, 다음 미들웨어를 호출하는 함수입니다.
err는 이전 미들웨어에서 발생한 에러를 전달받은 객체입니다.
req, res는 저희가 일반적으로 사용하는 HTTP 요청과 응답을 관리하는 객체입니다.
next는 다음 미들웨어를 실행하는 함수입니다.

   next  안에 에러 객체를 할당하면 (맨 앞의 에인자)에러처리 미들웨어로 전달됩니다.

1. 에러를 전달 받습니다.(인자 err)

2. console.error(err.stack)에서 에러 시지 출력합니다.

3. res.status(500)을 보내 서버에서 에러가 발생(500)했다는 것을 알립니다.

4. 클라이언트에게 'Something broke!'라는 메시지를를 전달합니다.

에러를 인자로 전달받아 클라이언트에게 에러 응답을 반환하거나 다음 미들웨어로 에러를 전달하는 역할을 담당합니다.

에러 처리 미들웨어는 Nest.js 프레임워크에서 Exception Filter라는 이름으로 불리고 있습니다. 

Experss.js에서는 미들웨어나 라우터에서 에러가 발생하면, 해당 에러를 next 함수를 통해 다음 미들웨어로 전달합니다. 그리고, Express.js는 등록된 미들웨어 중에서 에러를 매개변수로 받는 미들웨어(에러 처리 미들웨어)를 찾아 실행하게됩니다.

 

3) 에러 발생 시 에러를 처리하는 미들웨어 작성하기

이전에는 Joi를 이용해 에러가 발생하게된다면, try/catch문을 이용해 에러를 처리하는 작업을 진행하였습니다. 하지만, 해당 에러 처리는 할 일 생성 API만 사용하는 것이 아니라, 다른 API에서도 동일하게 데이터 유효성 검증을 진행할 것입니다.
그렇다면, 매번 try/catch문을 사용하여, 에러 처리를 위한 메시지를 작성해야할 것입니다. 이것은 너무 비효율적이고, 중복되는 코드가 추가되는 문제가 발생하게 됩니다. 이러한 문제를 해결하기 위해, 저희는 미들웨어를 이용하여 에러 처리를 통합적으로 관리할 예정입니다.

 

◆ 미들웨어를 이용한 통합적인 에러 처리관리

가장 먼저, middlewares 폴더를 생성하고, error-handler.middleware.js 파일에서 작업을 진행하도록 하겠습니다. 

// /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 파일에서 우리가 작성한 TodoRouter를 설정하는 코드 하단에 에러 처리 미들웨어를 등록하도록 하겠습니다.
에러 처리 미들웨어 등록하기

// app.js

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

...
// 라우터 등록 아래

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

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

에러 처리 미들웨어는 왜 Router 하단에 등록하는이유

미들웨어는 등록된 순서대로 실행됩니다. TodoRouter에서 비즈니스 로직을 수행한 후 발생한 에러는 다음 미들웨어로 전달됩니다. 이 때, 에러 처리 미들웨어가 라우터 이후에 등록되어 있으면, 에러를 잡아 처리할 수 있게 됩니다.
반대로, 라우터 이전에 에러 처리 미들웨어를 등록하면 라우터에서 발생한 에러를 처리할 수 없습니다.
왜냐하면, 라우터에서 발생한 에러는 라우터 이후에 등록된 미들웨어로 전달되기 때문입니다. 이렇게 되면 에러 처리 미들웨어가 에러를 잡지 못하게 되는것이죠. 따라서, 항상 에러 처리 미들웨어는 라우터 설정 코드 하단에 위치해야 합니다. ​

 

5) 할 일 등록 API 리팩토링

마지막으로, 미들웨어를 등록 완료하였다면, 할 일 등록 API의 코드 또한 에러 처리 미들웨어를 사용할 수 있도록 수정해야합니다. 만약, 에러가 발생하였다면, 다음 미들웨어로 에러를 전달할 수 있도록 코드를 수정해야겠죠?
routes/todos.router.js파일에서 에러 처리 미들웨어를 사용할 수 있도록 코드를 수정하겠습니다.


할 일 등록 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);
  }
});

 

더보기
// /routes/todos.router.js

import express from "express";
import Joi from "joi"; // joi import
import Todo from "../schemas/todo.schema.js";

// 라우터 생성
const router = express.Router();

/**할 일 생성 API 유효성 검사 요구사항
value 데이터는 필수적으로 존재해야한다.
value 데이터는 문자열 타입이어야한다.
value 데이터는 최소 1글자 이상이어야한다.
value 데이터는 최대 50글자 이하여야한다.
유효성 검사에 실패했을 때, 에러가 발생해야한다. = validateAsync로 검증하세요 */

// 할 일 생성 API의 요청 데이터 검증을 위한 Joi 스키마를 정의합니다.
// createTodoSchema는 Joi.object로 value를 검증합니다.
// value는 문자열 타입, 최소 1글자 이상 최대 50글자 이하, 데이터가 필수적으로 존재해야한다.
const createTodoSchema = Joi.object({
  value: Joi.string().min(1).max(50).required(),
});

/** 할일등록 API **/
router.post("/todos", async (req, res, next) => {
  try {
    // 클라이언트에게 전달받은 데이터를 검증합니다.
    // validateAsync : 애러 발생시키기 위해서 비동기메소드로 검증
    const validation = await createTodoSchema.validateAsync(req.body);
    // 검증 성공
    const { value } = validation;

    // 1-5. 데이터 유효성 검사 기능 추가
    if (!value) {
      return res
        .status(400) // 400 : 클라이언트 잘못으로 오류 발생
        .json({ errorMessage: "해야할 일 데이터가 존재하지 않습니다." });
    }

    // 2. 해당하는 마지막 order 데이터를 조회 (MongoDB에서 'order' 값이 가장 높은 '해야할 일')
    const todoMaxOrder = await Todo.findOne().sort("-order").exec();

    // 3. 만약 존재한다면 현재 할 일에 +1, order 데이터가 존재하지 않는다면 1로 할당합니다.('order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.)
    const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

    // 4. 해야할 일 등록
    // todo를 실제 인스턴스형식으로 만듦
    const todo = new Todo({ value, order });
    // 실제로 DB에 저장
    await todo.save();

    // 5. 해야할 일을 클라이언트에게 반환
    return res.status(201).json({ todo });
  } catch (error) {
    // 기존의 에러처리 코드를 /middlewares/error-handler.middleware.js로 옮기기

    // Router 다음에 있는 에러 처리 미들웨어를 실행한다.
    next(error);
  }
});

/** 해야할 일 목록 조회 API**/
// API등록은 router에서 합니다.
router.get("/todos", async (req, res, next) => {
  // 1. 해야할 일 목록 조회를 진행한다.
  const todos = await Todo.find().sort("-order").exec();

  // 2. 해야할 일 목록 조회 결과를 클라이언트에게 반환 한다.
  return res.status(200).json({ todos }); // 200 성공
});

/** 순서 변경, 할 일 완료/해제, 할 일 내용 변경 **/
// /:todoId' : 수정해야 할 데이터를 알기 위해 지정. 경로 매개변수
router.patch("/todos/:todoId", async (req, res) => {
  const { todoId } = req.params;

  // 완료여부확인을 위해 done 추가
  const { order, done, value } = req.body;

  const currentTodo = await Todo.findById(todoId).exec();
  if (!currentTodo) {
    return res
      .status(404) // 400은 클라이언트 잘못
      .json({ errorMessage: "존재하지 않는 해야할 일 입니다." });
  }

  // 해야할 일 순서 변경
  if (order) {
    const targetTodo = await Todo.findOne({ order }).exec(); // {order}는  {order:order}를 의미함. 객체의 값이라 {}사용
    if (targetTodo) {
      targetTodo.order = currentTodo.order; // order값 바꾸기.. 4->1이면 2,3번은 그대로 있고 4, 1번만 바뀜
      await targetTodo.save();
    }
    // 변경하려는 '해야할 일'의 order 값을 변경합니니다.
    currentTodo.order = order;
  }

  // 할일을 완료한 경우
  if (done !== undefined) {
    // false는 done에 값이 들어가지 않습니다.
    // 변경하려는 '해야할 일'의 doneAt 값을 변경합니다.
    // 위에서 todoId 조회를 위해 사용한 currentTodo필드에 doneAt라는 컬럼에 할당한다
    // 삼항문으로 done의 값이 있는 경우 그 값을 할당 합니다.
    currentTodo.doneAt = done ? new Date() : null;
  }

  /** 해야할 일 수정 */
  if (value) {
    // 변경하려는 '해야할 일'의 내용을 변경합니다.
    currentTodo.value = value;
  }

  // 변경된 '해야할 일'을 저장합니다.
  await currentTodo.save();

  return res.status(200).json({});
});

/** 할 일 삭제 **/
router.delete("/todos/:todoId", async (req, res) => {
  // 삭제할 '해야할 일'의 ID 값을 가져옵니다.
  // todoId : 경로 매개변수인 req.params에서 가져오기
  const { todoId } = req.params;

  // 삭제하려는 '해야할 일'을 가져옵니다.(조회)
  const todo = await Todo.findById(todoId).exec();
  //만약, 해당 ID값을 가진 '해야할 일'이 없다면 에러를 발생시킵니다.
  if (!todo) {
    return res
      .status(404) // 클라이언트 잘못. id 입력 오류
      .json({ errorMessage: "존재하지 않는 todo 데이터입니다." });
  }

  // 조회된 '해야할 일'을 삭제합니다.
  // MongDB의 데이터 id는 '_id'에 있습니다.
  // 코드에서 중복없이 발급되는 데이터 id를 컬럼todoId에 할당하고 있습니다.
  await Todo.deleteOne({ _id: todoId }).exec();

  return res.status(200).json({});
});

export default router;

할 일 등록 API에 next 매개 변수를 추가하였고, catch문에서 에러가 발생했을 때 처리하는 코드의 역할을 모두 에러 처리 미들웨어로 위임하였습니다.
이로써, 할 일 등록 API에서는 Joi 에러가  발생하든, MongoDB 에러가 발생하든 상관 없이 비즈니스 로직을 수행하는 것만 생각하면 되는 것입니다. ​

 

6) 폴더 구조 살펴보기

다음 챕터로 넘어가기 전에, [할 일 메모 사이트]프로젝트의 폴더 구조에 대해서 좀 더 자세히 알아보도록 하겠습니다.


우선, 저희는 아래와 같은 폴더 구조를 가지고 있는 프로젝트를 구성하였습니다.

[할 일 메모 사이트] Directory Structure
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 파일, 이미지 등 여러 파일들이 이 폴더에 위치하게 됩니다.


폴더 구조는 각각의 파일이 가지는 역할을 명확하게 분류하여 구성하는 것이 기본입니다. 
프로젝트가 커질수록 Layered Architecture, DDD(Domain Driven Development), Clean Architecture와 같은 여러가지 아키텍처 패턴을 통해 더 효율적으로 관리할 수 있답니다.

 

Express 4.x - API 참조

Access the API reference for Express.js 4.x, detailing all modules, methods, and properties for building web applications with this version.

expressjs.com