나의 발자취

[Node.js] MongoDB, Mongoose 활용해서 CRUD 기능 게시판 만들기 본문

Backend

[Node.js] MongoDB, Mongoose 활용해서 CRUD 기능 게시판 만들기

달모드 2024. 10. 18. 16:49

MongoDB?

MongoDB는 스키마가 없는 NoSQL 데이터베이스로, 각 문서가 서로 다른 구조를 가질 수 있다.

따라서 유저별로 각기 다른 프로필(e.g. Social Media)을 갖고 있을 때, 로그인 데이터나 이벤트 데이터 저장할때, 이벤트 조인이 필요없는 경우, 대규모 분산시스템(복잡한 트랜젝션), 실시간 데이터 분석, log data 처리 등은 몽고DB로 하면 효율적으로 관리할 수 있다.

 

특징 6개를 정리해 보았다.

 

1. 유연한 데이터 구조

이를 통해 유저별로 각기 다른 프로필 정보를 저장할 수 있다. 예를 들어, 사용자 A는 소셜 미디어 정보가 포함된 프로필을 가지고, 사용자 B는 다른 유형의 정보를 가질 수 있다.

 

2. 이벤트 조인이 필요없는 경우

MongoDB는 문서 지향 데이터베이스이기 때문에, 관련 데이터를 같은 문서 안에 저장할 수 있다. 예를 들어, 사용자 이벤트와 로그인 정보를 같은 문서에 저장하면, 조인 없이 쉽게 접근할 수 있다. 이런 경우에 MongoDB는 성능상 이점을 제공한다.

{
  "userId": "12345",
  "loginEvents": [
    {
      "timestamp": "2023-10-18T10:00:00Z",
      "ipAddress": "192.168.1.1"
    },
    {
      "timestamp": "2023-10-19T10:30:00Z",
      "ipAddress": "192.168.1.2"
    }
  ],
  "profile": {
    "name": "Alice",
    "socialMedia": {
      "twitter": "@alice",
      "facebook": "facebook.com/alice"
    }
  }
}

 

3. 대규모 분산 시스템

MongoDB는 수평적 확장이 가능하여, 데이터가 커질수록 서버를 추가함으로써 성능을 유지할 수 있다. 이는 대규모 트래픽을 처리해야 하는 애플리케이션에 적합하다.

 

4. 복잡한 트랜잭션

MongoDB는 4.0 버전 이후 ACID 트랜잭션을 지원하여, 여러 문서에 걸친 복잡한 트랜잭션을 안전하게 처리할 수 있다. 하지만, 전통적인 관계형 데이터베이스보다 트랜잭션을 관리하는 방식이 다르므로, 필요에 따라 적절한 설계가 필요하다.

 

5. 실시간 데이터 분석

MongoDB는 대량의 데이터를 실시간으로 처리하고 분석하는 데 적합하다. 예를 들어, 웹사이트에서 사용자 행동 데이터를 수집하고 실시간으로 분석하여 추천 시스템을 구현할 수 있다.

 

6. 로그 데이터 처리

로그 데이터를 저장하고 처리할 때도 MongoDB가 유용하다. 로그 데이터는 종종 구조가 일관되지 않으며, 빠르게 쌓일 수 있기 때문에 MongoDB의 스키마 유연성이 큰 장점이 된다.

 

일단 냅다 몽고DB 기초부터 훑고 시작

brew update

brew tap mongodb/brew  

brew install mogodb-community@7.0

를 터미널에 입력한다.

 

 

위에 설치하려면 아래 명령을 치라고 하니까 아래 명령어를 터미널 입력.

brew install mongodb/brew/mongodb-community@7.0

 

하면 설치가 된다. 

그러면 또 아래처럼 몽고디비를 시작하려면 명령어를 치래니까, 입력해준다.

brew services start mongodb/brew/mongodb-community@7.0

 

 

 

터미널에 mongosh 입력(mongo shell이라는 뜻이다)

 

순서대로 db, use userdb, show collections 입력

아무 테이블이 없으므로 생성해준다.

db.createCollection("users")

 

그러면 아래와 같이 { ok: 1 } 이 나온다.

 

그리고 insert문으로 DB에 데이터 하나를 넣어준다.

insert

db.users.insertOne({name:"lee", email:"lee@gmail.com", age:38, city:"Seoul"})

 

select문으로 DB에 잘 들어갔나 확인한다.

db.users.find() 입력

보면 JSON 형식과 매우 비슷한 것을 알 수 있다.

 

샘플을 하나 더 집어넣어준다. 이번에는 스키마를 정하지 않고 schemaless하게 넣어준다.

db.users.insertOne({name:"lee", age:"45"})

 

db.users.find() 로 모든 유저들 조회

 

보면 스키마less하게 집어넣어도 데이터가 집어넣어진다는 것이 몽고DB의 큰 장점이다.

또한, 여러 개의 클러스터를 집어넣어줄 수 있다는 장점이 있다.

 

db.users.insertMany([{name:"choi1", age:10}, {name:"choi2", age:9}])

 

비교 부등호 (조건식)

db.users.find({ age: {$gt: 40 } } )

-> gt는 greater than 이라는 것이다.

그럼, lt: less than 겠지...

 

db.users.find({ age: {$lt: 40 } } )

 

 

같다는 조건은 e를 쓴다.

lt는 < 인데,

lte 는 <=

 

업데이트

db.users.updateOne({name: "lee"},{$set:{age:48}})

 

 

 

삭제

db.users.deleteOne({name:"lee"})

 

탈출방법:  exit

 

이렇게 몽고DB의 경우 스키마 없이 데이터를 집어넣었지만 몽구스의 경우, 스키마가 있어야 하고 게시판은 몽구스를 활용해서 생성해볼 것이다.

 

 


프로젝트 시작

https://github.com/est22/backend/commits/main/nodejs/09/ch09_03

 

GitHub - est22/backend

Contribute to est22/backend development by creating an account on GitHub.

github.com

 

npm init -y

npm i express nodemon mongoose

 

 

app.js 파일을 생성 후, mongoose 사용할 준비를 해준다.

const express = require("express");
const mongoose = requrie("mongoose");
mongoose.connect("mongodb://localhost/facebook");

 

 

그리고 아래에 mongoose의 스키마를 짜준다.

테이블명은 facebook, 모델 명은 Post이다.

 

const express = require("express");
const mongoose = requrie("mongoose");
mongoose.connect("mongodb://localhost/facebook");

const db = mongoose.connection;
db.on("error", (err) => {
  console.error(`mongo connect error: ${err}`);
});
db.once("open", () => {
  console.log(`mongo connected successfully`);
});

const PostSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: String,
  createdAt: { type: Date, default: Date.now },
  comments: [
    {
      comment: String,
      author: String,
      createdAt: { type: Date, default: Date.now },
    },
  ],
});

const Post = mongoose.model("Post", PostSchema);

 

그리고 이어서 모델을 받아서 라우터에 띄워주고, 

app.post() 엔드포인트 작업을 해준다.

const Post = mongoose.model("Post", PostSchema);
const app = express();
app.use(express.json());

app.post("/posts", async (req, res) => {
  const { title, content, author } = req.body;

  try {
    const post = new Post({
      title: title,
      content: content,
      author: author,
    });
    post.save();
    res.status(201).json({ data: post });
  } catch (e) {
    res.status(500).json({ error: error });
  }
});
app.listen(3000);

 

이제 포스트맨에 가서 포스트 입력 테스트를 해준다.

 

 

{
"title":"test",
"content":"content",
"author": "lia"

 

}
와 비슷한 내용으로 여러개 만들어준다.

 


그리고 이어서 

모든 게시글 가져오기: app.get("/posts") 엔드포인트 작업

app.get("/posts", async (req, res) => {
  try {
    const posts = await Post.find({});
    res.json({ data: posts });
  } catch (e) {
    res.status(500).json({ error: e });
  }
});

 

특정 게시글 가져오기: app.get("/posts/:id") 엔드포인트 작업

app.get("/posts/:id", async (req, res) => {
  const id = req.params.id;
  try {
    const post = await Post.findById(id);
    res.json({ data: post });
  } catch (e) {
    res.status(500).json({ error: e });
  }
});

 

(+ findById(id) 로 넣어야하고, findById({id}) < 이렇게 객체로 넣으면 가져올 수 없다고 에러가 뜨니 주의)

 

여기서의 id는 몽고DB가 생성해준 id가 되어야한다.

따라서 포스트맨에서 테스트를 할 때, 몽고 DB에서 데이터를 POST 할 때에 생성해준 id값을 붙여넣는다.

여기서는 _id 값이 671201fd496694399bed4803 이므로, GET 요청 엔드포인트로는 id값의 위치에 이 값을 넣어서 요청한다.

 

그러면 아래와 같이 id값에 해당하는 데이터를 잘 가져오는 것을 볼 수 있다 :)

 

게시글 수정: app.put("/posts/:id") 엔드포인트 작업

mongoose에서는 id값으로 찾은 다음 업데이트를 해주는 findByIdAndUpdate() 메서드가 있다 :)

app.put("/posts/:id", async (req, res) => {
  const { id } = req.params;
  const { title, content } = req.body;
  try {
    const post = await Post.findByIdAndUpdate(id, {
      title,
      content,
    });
    res.status(200).json({ data: post });
  } catch (e) {
    res.status(500).json({ error: e });
  }
});

 

 

그리고 새롭게 내용을 수정하여 테스트~! 해준다.

이때는 author가 없다는 점.

{
"title": "[edited] 😊 Learning New Skills 😊",
"content": "I'm taking an online course to improve my coding."
}

 

그러면 아래와 같이 수정 반영?..;;; 

 

 
 

 

삭제: app.delete("/posts/:id") 엔드포인트 작업

app.delete("/posts/:id", async (req, res) => {
  const { id } = req.params;

  try {
    const post = await Post.findByIdAndDelete(id);
    res.status(200).json({ data: post });
  } catch (e) {
    res.status(500).json({ error: e });
  }
});

 


댓글 기능: app.post("/posts/:id/comments") 엔드포인트 작업

app.post("/posts/:id/comments", async (req, res) => {
  const { id } = req.params;
  const { comment, author } = req.body;
  try {
    const post = await Post.updateOne(
      {
        _id: id,
      },
      {
        $push: { comments: { comment: comment, author: author } },
      }
    );
    res.status(200).json({ data: post });
  } catch (e) {
    res.status(500).json({ error: e });
  }
});

 

1. _id 란?

 

  • MongoDB의 기본 키: _id는 MongoDB에서 각 문서를 유일하게 식별하기 위한 기본 필드이다. (포스트맨에서 본 것처럼) 이 값은 각 문서에 대해 자동으로 생성되며, 보통 ObjectId 타입이다.

2. $push란?

  • 배열에 요소 추가: $push는 MongoDB의 업데이트 연산자 중 하나로, 지정된 배열 필드에 새로운 요소를 추가하는 데 사용된다. 이 연산자를 사용하면 기존의 배열에 새로운 데이터를 쉽게 추가할 수 있다.
  • 예제: 코드에서는 comments 배열에 새로운 댓글을 추가하고 있다. 즉, 해당 포스트의 comments 필드에 comment와 author 정보를 담은 객체를 추가하게 된다.

 

테스트

위에서 수정한 게시글에 댓글을 달것이다.(id = 671201fd496694399bed4803였음)

 

/POST http://localhost:3000/posts/671201fd496694399bed4803/comments

엔드포인트로 요청을 한다.

 

GET 요청을 해서 방금 내가 댓글을 단 게시글을 확인해보면, 아래와 같이 정상적으로 게시글이 수정된 것도 보이고, 댓글도 보인다!

 

다만, 두 개의 데이터가 보이는 것은 원래 댓글을 달 때 body로 보내는 json 객체 데이터 안의 키값이 "comment"라고 전송되었어야하는데, 처음에는 "comments"로 보내고 그다음 수정한 후에 한번 더 보내서 그렇다.

NoSQL 특징상 스키마가 없어도 있는 스키마는 이렇게 등록이 되므로.. 잘 지켜줘야한다는 점... 

 

댓글 삭제 기능: app.post("/posts/:id/comments/:cid") 엔드포인트 작업

마지막으로 댓글 삭제

app.delete("/posts/:id/comments/:cid", async (req, res) => {
  const { id, cid } = req.params;

  try {
    const post = await Post.updateOne(
      {
        _id: id,
      },
      {
        $pull: { comments: { _id: cid } },
      }
    );
    res.status(200).json({ data: post });
  } catch (e) {
    res.status(500).json({ error: e });
  }
});

 

아까 댓글을 $push 했을때의 결과를 잠깐 보면, 아래와 같이 "comments" 안에 각 댓글마다 "_id"값을 가지고 있는 것이 보인다.

 

따라서 내가 삭제할 게시글(id)의 댓글(cid = 

$pull: { comments: { _id: cid } },

 

로 해주는 것이다.

 

아까 잘못 입력한 댓글의 id값을 집어넣어서 삭제해주겠다.

/DELETE http://localhost:3000/posts/671201fd496694399bed4803/comments/67120e8b5cb803fb67a357a5

 

를 해준다.

 

잘 삭제되었는지 다시 아까 GET 요청을 해준다.

 

위와 같이 댓글이 잘 삭제된 것을 볼 수 있다 :) 

 

여기서 질문

 

왜 delete인데도 updateOne()을 쓰나?

아래처럼 deleteOne()을 쓰면, 

const post = await Post.deleteOne({
    _id: id,
    "comments._id": cid,
});

 

이 경우 댓글을 삭제하는 것이 아니라 해당 포스트 전체를 삭제하는 결과가 된다. 즉, 이 변경은 원래의 의도와는 다르다..!! 

 

테스트

 

위의 실습 순서대로 코드를 작성한 이력은 아래 깃허브 레포지토리 링크에서 확인할 수 있다.

https://github.com/est22/backend/commits/main/nodejs/09/ch09_03

 

Comments