Node 강의/입문

2-2 웹 서버 + MongoDB 실습

kagan-draca 2024. 9. 2. 20:59

 

Express를 통해 MongoDB를 사용하고, REST API를 설계하고 구현하는 작업을 진행한다.

 

1. 단계 : 할 일 메모 페이지 만들기( 프론트엔드로 부터 제공 받을 예정 )

 

2. 단계 : Express.js 서버 : API를 구현하기 위해 Express.js를 이용해 서버 코드 작성하기

 

3. 단계 : MongoDB, mongoose : 할 일 목록을 저희가 대여한 MongoDB에 저장하기

 

2. 단계 : 할 일 메모 페이지 만들기

 

먼저 server와 통신할 .js 파일에

 

import express from "express";

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

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(express.static("./assets"));

//const router = express.Router();

app.get("/", (req, res) => {
  res.send("Hello World");
});

//app.use("/api", router);

app.listen(PORT, () => {
  console.log(PORT, "에 접속했습니다");
});

 

으로 작성해준다.

 

 

./assets 파일 안에는 css, html 파일이 존재합니다.

 

 

app.use(express.static("./assets"))

 

는 app.js 파일 기준으로, 입력 값("./assets") 경로에

있는 파일을 아무런 가공 없이 그대로 전달해주는 미들웨어입니다.

그래서,

 

URL에 http://localhost:3000/을 입력할 경우

아무런 경로를 입력하지 않아도

 

"./assets" 파일 내부에 있는 index.html이 기본으로

화면에 출력되게 됩니다.

 

 

 

3. 단계 : MongoDB, mongoose

 

 

이제 mongoDB와 연결하기 위해

 

schemas 라는 폴더를 만들고,

index.js 파일을 만들어줍니다.

 

index.js 파일 안에

import mongoose from "mongoose";

let connect = () => {
  mongoose
    .connect(
      "몽구 DB 주소와 아이디 비밀번호",
      {
        dbName: "사용할 DB 이름",
      }
    )
    .catch(err => console(err))
    .then(() => console.log("몽구 DB 연결 성공"));
};

export default connect;

작성해줍니다.

 

이제 할 일 메모사이트 Schema 설계를 해봅시다.

 

(중요)개념적 스키마

 

 

개념적 스키마를 위와 같이 설계할 것이고,

 

또한,

 

(중요)논리적 스키마를

 

이제 할 일 메모 사이트의 기능을 바탕으로 필요한 데이터를 도출해보도록할게요!

  1. 할 일 추가하기, 목록 보기, 내용 변경 기능:
    • 할 일을 구현하기 위해선, 내가 어떠한 할 일이 있는지 내용이 필요하겠죠?
    • 여기서, 해야 할 일(value)이라는 문자열(String) 형식의 필드(Field)를 정의하겠습니다.
  2. 할 일 순서 변경하기 기능:
    • 할 일의 순서를 구현하기 위해선, 할 일이 몇번째 해야할 일인지 정의가 되어 있어야합니다.
    • 여기서, 해야 할 일의 순서(order)라는 숫자(Number) 형식의 필드(Field)를 정의하겠습니다.
  3. 할 일 완료하기, 완료 해제하기 기능:
    1. 마지막으로, 할 일을 완료했다면, 언제 완료했는지 시간을 확인할 수 있어야할 것입니다.
    2. 여기서, 완료 날짜(doneAt)라는 날짜(Date) 형식의 필드(Field)를 정의하겠습니다.

 

위와 같이 설계 했습니다.

 

(중요)물리적 스키마

 

최종적으로 할 일(Todo) 스키마의 요소들은 아래와 같습니다.

  1. 해야 할 일(value):
    • 할 일의 내용을 나타내는 문자열(String) 형식의 데이터입니다.
  2. 해야 할 일의 순서(order):
    1. 할 일의 순서를 나타내는 숫자(Number) 형식의 데이터입니다.
  3. 완료 날짜(doneAt):
    • 할 일이 언제 완료되었는지 나타내는 날짜(Date) 형식의 데이터입니다.
    • 완료되지 않았다면 null, 완료 되었다면 날짜(Date) 형식의 데이터를 가지게됩니다.

 

위와 같습니다.

 

이제 스키마를 위한 .js 파일을 작성해봅시다.

 

이름은 todo.schema.js로  DB의 스키마를 만들 줄 것 입니다.

 

todo.schema.js

 

import mongoose from "mongoose";

// 물리적인 스키마를 바탕으로 작성
const TodoSchema = new mongoose.Schema({
  value: {
    type: String, // DB에 저장될 데이터 타입
    required: true, // DB에 필수 요소 표기
    // MYSQL에서 NOT NULL 같은 개념
  },
  order: {
    type: Number,
    required: true, // DB에 필수 요소 표기
    // MYSQL에서 NOT NULL 같은 개념
  },
  doneAt: {
    type: Date,
    required: false, // DB 필수 요소는 아님
    // NULL(데이터 없음) 가능
  },

  // value, order, doneAt 하나하나가
  // DB에서 fields 또는 colum이 된다.

  // value, order, doneAt에 해당 하는
  // 데이터가 생긴다면 그 데이터를 tupple, row라고 부른다.
});

// 프론트엔드로 서빙하기 위한 코드(몰라도 괜찮다!!)
TodoSchema.virtual("todoId").get(() => {
  return this._id.toHexString();
});

// 프론트엔드로 서빙하기 위한 코드(몰라도 괜찮다!!)
TodoSchema.set("toJSON", {
  virtual: true,
});

// TodoSchema를 바탕으로 Todo 모델을 생성하여, 외부로 내보냅니다.
export default mongoose.model("Todo", TodoSchema);

 

위와 같은 코드로 하나의 field에

데이터 타입과 NULL 유무를 작성해줘야 합니다.

 

이제 API를 구축해봅시다

 

API를 구축하기 위한 todo.router.js 파일을 만들어 줍니다.

 

todo.router.js

 

import express from 'express';

const router = express.Router();


export default router;

 

 

express의 Router를 이용한 미들웨어 통신 골격을 생성한 후,

 

할일 등록 API를 Router를 통해 만들어보겠습니다.

할일 등록 API

 

// 할일 등록 API
// (중요) async를 줘서 비동기 함수로 구현한다.
router.post("/todos", async (req, res) => {
  const { value } = req.body;
  // 클라이언트(HTML)에서 입력 받은 값을 가져와 저장한다.

  const todoMaxOrder = await Todo.findOne().sort("-order").exec();
  //           await로 DB에서 순위를 가져올 때 까지
  //           아래 작업을 진행하지 않는다.
  //           findOne은 한 개의 데이터만 조회할 때 사용한다.
  //           sort 정렬한다. -를 붙으면 내림차순, 안 붙이면 오름차순
  //           exec()는 꼭 붙여 사용하는 것이 좋다

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

  const todo = new Tode({ value, order });
  //           class 객체로 생성

  await todo.save();
  // 실제 데이터 베이스에 저장한다.

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

 

exec() 메서드는 왜 사용하는건가요?

 

mongoose에서 exec()는 결과를 반환하기 위해 쿼리를 실행하고,

이 결과로 Promise를 반환하게 됩니다.

 

만약, exec() 메서드를 사용하지 않는다면, 해당 쿼리는 결과값이 Promise로 반환되지 않기 때문에,

아직 데이터 전달받지 않은 상태에서 다음 코드를 실행하게 될 수 있습니다.

 

결국, 사용하려는 데이터가 NULL로 정의 돼, 우리가 가장 많이 보게 될 

 

TypeError

 

발생하게 됩니다.

 

 

데이터 유효성 검사 기능 추가하기

 

데이터 유효성 검사(Validation) : 전달받은 데이터가 예상한 형식과 일치하는지 확인하기 위한 작업

 

ex) 해야할 일(value)가 데이터가 비어있다면?

 

위아 같은 에러가 발생하게 됩니다.

 

때문에, value 데이터의 유무를 미리 판단하고,

데이터가 존재하지 않을 때,

 

클라이언트에게 현재 상황을 전달함과 동시에

더 이상 다음 코드를 실행되지 않도록 코드를 구현해야합니다.

 

router.post("/todos", async (req, res) => {
  const { value } = req.body;
  // 클라이언트(HTML)에서 입력 받은 값을 가져와 저장한다.

  if (!value) {
    return res
      .status(400)
      .json({ errorMessge: "해야할 일 데이터가 존재하지 않습니다" });
    // value가 존재하지 않을 때, 클라이언트에게 에러 메시지를 전달합니다.
  }

  const todoMaxOrder = await Todo.findOne().sort("-order").exec();
  //           await로 DB에서 순위를 가져올 때 까지
  //           아래 작업을 진행하지 않는다.
  //           findOne은 한 개의 데이터만 조회할 때 사용한다.
  //           sort 정렬한다. -를 붙으면 내림차순, 안 붙이면 오름차순
  //           exec()는 꼭 붙여 사용하는 것이 좋다

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

  const todo = new Todo({ value, order });
  //           class 객체로 생성

  await todo.save();
  // 실제 데이터 베이스에 저장한다.

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

 

 해야 할 일(value) 정보가 존재하는지 확인하고,

value가 undefined나 빈 문자열인 경우,

 

서버에 400(Bad Request) 상태 코드와 함께

에러 메시지를 전달하게 됩니다.

 

이렇게 구현된 할 일 등록 API를 Client API인

Insomnia에서 테스트해봅시다.

 

Clinet API인 Insomnia에서 테스트

 

 

JSON 형식으로

 

{

    value : "밥 먹기"

}

 

를 주었을 때 정상적으로 추가 되는걸 확인할 수 있습니다.

 

 

할 일 목록 조회 API

 

할 일 목록 조회는 order을 기준으로 내림차순해서 Todo 데이터를 가져올 겁니다.

 

// 해야할 일 순서 변경 API
router.get("/todos", async (req, res) => {
  // Todo 모델을 이용해, MongoDB에서 값이 가장 높은 order을 찾습니다
  let todos = await Todo.find().sort("-order").exec();
  //                  find함수로 찾은 모든 데이터를
  //                  -order로 내림차순해서 가져옵니다.

  // 찾은 "해야할 일"을 클라이언트에게 전달
  return res.json({ todos });
});

 

Clinet API인 Insomnia에서 테스트

 

조회를 해보면 우리가 집어 넣은 데이터가 모두 출력되는걸 확인할 수 있다.

 

이제 해야 할 일의 순서 변경, 내용 변경, 완료/해제 여부 Update로 구현해보겠습니다.

 

할 일 목록 수정 API

 

먼저 할 일 순서 변경 API 부터 만들겠습니다.

 

router.patch("/todos/:todoId", async (req, res) => {
  const { todoId } = req.params;
  const { order } = req.body;

  // 현재 나의 order가 무엇인지 알아야한다.
  const currentTodo = await Todo.findById(todoId).exec();
  //            findById()를 통해 todoId에 해당하는 id를 가져올 것이다.

  if (!currentTodo)
    return res
      .status(404)
      .json({ errorMessge: "존재하지 않는 해야할 일 입니다." });

  if (order) {
    const targetTodo = await Todo.findOne({ order }).exec();
    //                  findOne({order})를 줘서 찾고자하는
    //                  데이터 값만을 가져 옵니다.
    //                  Todo.findOne({order : order}) 와 동일
    //                  객체 분해할당 조립으로 생략함

    if (targetTodo) {
      // 변경 대상이 존재할 때만 변경
      targetTodo.order = currentTodo.order;
      // 변경 대상의 순위를 현재 해야하는 순위로 바꾼다.
      // (할 일을 앞 당겨준다.)
      await targetTodo.save();
      // 실제 데이터 베이스에 저장
    }

    currentTodo.order = order;
    // 현재 순서의 대상을 내가 지정한 순서로 변경
    // (할 일을 뒤로 미뤄준다.)
  }

  await currentTodo.save();

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

 

현재 DB안에 있는 JSON 형식 데이터를 보면

 

 

위와 같이 존재하는데,

 

value가 "밥 먹기"인 order를 4로 Insomnia에서 변경해보겠습니다.

 

그러기 위해서 value가 "밥 먹기"인 데이터의

 

id인 66d6846a23903ba4c4c40629을 가져옵니다.

const currentTodo = await Todo.findById(todoId).exec();

에서 todoId가 사용되기 때문에

 

Insomnia에서 변경해야할 설정

우리는 "밥 먹기" order를 바꿔줄 예정이므로,

 

PATCH 항목으로 변경 후,

URL과 "밥 먹기"의 Id를 가져옵니다.

 

그리고, Body에서 JSON 파일 형식으로

 

{

    "order" : 원하는 순위

}

 

로 바꿔줍니다.

 

그 결과

 

 

밥 먹기와 "공부하기" 순위가 변경된 걸 볼 수 있습니다.

 

이 기능을 추가해, 할 일 완료/해제를 구현해보겠습니다.

 

할 일 완료/해제 API 구현하기

 

API는 done이라는 값을 전달받아 할 일 완료/해제 기능을 구현한다.

 

// 해야할 일 순서 변경, 완료/해제 API
router.patch("/todos/:todoId", async (req, res) => {
  const { todoId } = req.params;
  const { order, done } = req.body;

  // 현재 나의 order가 무엇인지 알아야한다.
  const currentTodo = await Todo.findById(todoId).exec();
  //            findById()를 통해 todoId에 해당하는 id를 가져올 것이다.

  if (!currentTodo)
    return res
      .status(404)
      .json({ errorMessge: "존재하지 않는 해야할 일 입니다." });

  if (order) {
    const targetTodo = await Todo.findOne({ order }).exec();
    //                  findOne({order})를 줘서 찾고자하는
    //                  데이터 값만을 가져 옵니다.
    //                  Todo.findOne({order : order}) 와 동일
    //                  객체 분해할당 조립으로 생략함

    if (targetTodo) {
      // 변경 대상이 존재할 때만 변경
      targetTodo.order = currentTodo.order;
      // 변경 대상의 순위를 현재 해야하는 순위로 바꾼다.
      // (할 일을 앞 당겨준다.)
      await targetTodo.save();
      // 실제 데이터 베이스에 저장
    }

    currentTodo.order = order;
    // 현재 순서의 대상을 내가 지정한 순서로 변경
    // (할 일을 뒤로 미뤄준다.)
  }

  if (done !== undefined) currentTodo.doneAt = done ? new Date() : null;
  // done이 undefined가 아닐 경우 done을 비교해
  // true이면 완료한 날짜와 시간을
  // null이나 false일 경우 null을 넣어줍니다.
  await currentTodo.save();

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

 

if (done !== undefined) currentTodo.doneAt = done ? new Date() : null;

 

기존 순위 변경 코드에서 위의 코드를 추가하는 방법으로 구현 됐습니다.

 

 

다시 "밥 먹기"의 순위를 2등과 교환할 것이고,

수행 여부(doneAt)를 true로 변경해보겠습니다.

 

 

                                           (바꾸기 전)                                                                                    (바꾼 후)

 

 

위와 같이 "밥먹기"와 "잠자기" 순위가 바뀌고,

"doneAt" Key의 Value로 날짜와 시간이 추가된 걸 볼 수 있습니다.

 

마찬가지로, 해야할 일 내용 변경도 코드를 추가하면 쉽게 구현할 수 있습니다.

 

할 일 내용 변경 API 구현하기

 

// 해야할 일 순서 변경, 완료/해제, 내용 변경 API
router.patch("/todos/:todoId", async (req, res) => {
  const { todoId } = req.params;
  const { order, doneAt, value } = req.body;

  // 현재 나의 order가 무엇인지 알아야한다.
  const currentTodo = await Todo.findById(todoId).exec();
  //            findById()를 통해 todoId에 해당하는 id를 가져올 것이다.

  if (!currentTodo)
    return res
      .status(404)
      .json({ errorMessge: "존재하지 않는 해야할 일 입니다." });

  if (order) {
    const targetTodo = await Todo.findOne({ order }).exec();
    //                  findOne({order})를 줘서 찾고자하는
    //                  데이터 값만을 가져 옵니다.
    //                  Todo.findOne({order : order}) 와 동일
    //                  객체 분해할당 조립으로 생략함

    if (targetTodo) {
      // 변경 대상이 존재할 때만 변경
      targetTodo.order = currentTodo.order;
      // 변경 대상의 순위를 현재 해야하는 순위로 바꾼다.
      // (할 일을 앞 당겨준다.)
      await targetTodo.save();
      // 실제 데이터 베이스에 저장
    }

    currentTodo.order = order;
    // 현재 순서의 대상을 내가 지정한 순서로 변경
    // (할 일을 뒤로 미뤄준다.)
  }

  // 할 일 내용 수정
  if (value) currentTodo.value = value;

  if (doneAt !== undefined) currentTodo.doneAt = doneAt ? new Date() : null;
  // done이 undefined가 아닐 경우 done을 비교해
  // true이면 완료한 날짜와 시간을
  // null이나 false일 경우 null을 넣어줍니다.
  await currentTodo.save();

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

 

기존 코드에서

 

// 할 일 내용 수정
if (value) currentTodo.value = value;

만 추가하는 형식으로 구현했습니다.

 

만약, "공부하기"를 "놀기" 수정할 경우,

("공부하기" 객체의 id가 필요하다)

(66d69b0ad01bbf2d5e8b493a)

 

                                           (바뀌기 전)                                                                                     (바뀐 후)

 

 

위와 같이 "공부하기"가 "놀기"로 바뀐 것을 볼 수 있습니다.

 

할 일 삭제 API 구현하기

 

API는 삭제할 할 일의 ID를 클라이언트에게 전달받아, MongoDB에서 해당 데이터를 삭제 합니다.

 

// 할 일 삭제
router.delete("/todos/:todoId", async (req, res) => {
  const { todoId } = req.params;

  const todo = await Todo.findById(todoId).exec();

  //  해당하는 일이 없을 경우
  if (!todo)
    return res
      .status(404)
      .json({ errorMessge: "존재하지 않는 해야할 일 정보입니다." });

  await Todo.deleteOne({ _id: todo.id });
  // Todo.deleteOne으로 찾고자 하는 id에 해당하는
  // 객체를 제거해줍니다.

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

 

위의 코드를 바탕으로  Insomnia에서 "밥먹기" 객체를 삭제 해보겠습니다.

 

 

 

위와 같이 밥 먹기가 제거된 것을 볼 수 있습니다.