Home MongoDB-환경설정과-간단한-CRUD구현하기
Post
Cancel

MongoDB-환경설정과-간단한-CRUD구현하기

강의 링크


MongoDB는 NoSQL 데이터 베이스 중 하나이다. NoSQL은 Not Only SQL의 줄임말로 기존의 RDBMS의 한계를 극복하기 위해 만들어진 새로운 형태의 데이터 저장소이다.

관계형 DB는 표를 저장한다면 MongoDB는 객체를 저장한다고 생각하면 된다.

즉, 관계형 DB가 아니므로, RDMS처럼 고정된 스키마 및 JOIN이 존재하지 않는다.

  • 데이터의 구조가 달라도 같은 테이블에 저장할 수 있다. (Schemaless)
  • 관계형 베이스: database > table > row
  • MogoDB: database > collection > document


1. Node.js와 MongoDB

node.js는 비동기 프로그래밍이기 때문에 Node.js로 MangoDB를 다룰 때 이를 유의해야한다.

특히, 동기적으로 처리해야 하는 경우와 비동기적으로 처리해도 괜찮은 경우를 알고 코딩을 해야 한다.

또한, Blocking이 많은 CPU Intensive한 작업들은 node.js에 맞지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
async function createBlog(){
  await User.findOne({...})
                      
  // 비동기적으로 처리해도 될 때는 Promise.all로 묶으면 된다.
  // 효율적으로 cpu를 사용하기 위해.. 
  await Promise.all([{
    Blog.insertOne({...}),
    User.updateOne({...})
  }])
  await LogApi({...})
  return "success"
}


1) 환경 설정

공식 문서

mongoose는 Node.js와 MongoDB를 연결해주는 ODM으로 mongoose를 사용하면 좀 더 편리하게 MongoDB를 사용할 수 있다.

1
npm install mongoose


다음과 같이 DB를 연결해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/server.js

import dotenv from "dotenv";
import express from "express";
import mongoose from "mongoose";
import userRouter from "./routes/userRoute.js";

dotenv.config();
const PASSWORD = process.env.PASSWORD;

const app = express();

// mongodb compass connect uri
const MONGO_URI = `mongodb+srv://admin:${PASSWORD}@mongodbtutorial.5pazsio.mongodb.net/?retryWrites=true&w=majority`;

const server = async () => {
  try {
    mongoose.set("strictQuery", false);
    
    // DB 연결
    let mongodbConnection = await mongoose.connect(MONGO_URI);
    mongoose.set("debug", true);
    console.log("MongoDB connected");

    // json을 객체로 파싱하는 미들웨어
    app.use(express.json());

    // Router (userAPI와 연결)
    app.use("/user", userRouter);

    // 클라이언트가 3000포트로 접속할 수 있다. (node Server)
    app.listen(3000, function () {
      console.log("server start: http://localhost:3000/");
    });
  } catch (error) {
    console.log(error);
  }
};

server();


2) Schema, Model 생성

앞서 말했듯이, Mongoose는 Table이 없기 때문에, 오타로 인한 다른 데이터나 타입이 데이터베이스에 저장되는 경우가 발생할 수 있다.

이러한 문제를 막기 위해 몽구스는 Schema를 도입했다.

  • Mongoose는 사용자가 작성한 스키마를 기준으로 데이터에 DB를 넣기 전에 먼저 검사를 한다.
  • Schema는 어느정도 테이블의 역할을 한다고 보면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/models/User.js

import { Schema, model } from "mongoose";

// new mongoose.Schema(스케마 정의, 추가 옵션)
const UserSchema = new Schema(
  {
    username: { type: String, required: true, unique: true },
    name: {
      first: { type: String, required: true },
      last: { type: String, required: true },
    },
    age: Number,
    emain: String,
  },
  { timestamps: true }
);

// 만든 스케마를 몽구스에게 알려주어 모델을 생성한다.
// model(콜렉션이름, 스키마)
const User = model("user", UserSchema);

export default User;


이렇게 만든 콜렉션에 도큐먼트를 추가하는 API를 만드려면 다음과 같이 코드를 작성하면 된다.

앞서 말했듯이, Node.js는 비동기 프로그래밍이기 때문에 데이터관련 작업 결과가 끝날 때까지 기다려야함을 유의해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/routes/userRoute.js

app.post("/user", async (req, res) => {
  try {
    // 유효성 검사
    let { username, name } = req.body;
    if (!username) return res.status(400).send({ error: "username is required" });
    if (!name || !name.first || !name.last)
      return res.status(400).send({ error: "Both first and last names are required" });

    const user = new User(req.body);

    // 데이터 저장 
    await user.save();
    return res.send({ user });

  // 에러 처리
  } catch (error) {
    console.log(error);
    return res.status(500).send({ error: error.message });
  }
};

  • 먼저, post 요청을 받으면 요청에 대한 유효성 검사를 실행한다.
    • 예시의 경우에는 필수 값인 username과 name 필드 값이 있는지 확인한다.
  • 에러가 없고 요청에 제대로 왔으면, 데이터를 db에 저장한다.
  • 저장이 완료된 후 응답을 보낸다.


3) 간단한 CRUD 작업하기

공식 문서

mongoose의 메서드를 사용하면, SQL 문법을 잘 몰라도 쉽게 DB에 접근하고 데이터를 CRUD할 수 있다.

mongoose에서 제공하는 메서드는 위의 공식 문서에 잘 정리되어있다.


:white_check_mark: find, findOne, findById

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/routes/userRoute.js

// user 데이터를 전부 반환하는 get 요청
app.get("/user", async (req, res) => {
  try {
      // find({필터 조건}): 탐색한 결과의 배열을 리턴한다.
      // findOne({필터 조건}): 탐색한 결과 하나를 리턴한다.
      const users = await User.find({});
      return res.send({ users });
  } catch (error) {
      console.log(error);
      return res.status(500).send({ error: error.message });
  }
});


// 특정 id의 user 데이터를 반환하는 get 요청
app.get("/user/:userId", async (req, res) => {
  try {
      const { userId } = req.params;

      // userId가 ObjectId에 맞는 형식인지 유효성 검사
      if (!mongoose.isValidObjectId(userId)) {
          return res.status(400).send({ error: "Invalid userId" })        
      }

      const user = await User.findOne({ _id: userId });
      return res.send({ user });
  } catch (error) {
      console.log(error) 
      return res.status(500).send({ error: error.message });
  }
});
  • Model.find({필터 조건}): 탐색한 결과의 배열을 반환한다.
  • Model.findOne({필터 조건}): 탐색한 결과 처음 찾은 도큐먼트를 반환한다.
  • Model.findById(id): 해당 ID를 가진 도큐먼트를 반환한다.
  • moongoose.isValidObjectId(데이터): 해당 데이터가 objectId에 맞는 형식인지 검사 후 boolean값을 반환한다.


:white_check_mark: delete, findOneAndDelete, findByIdAndDelete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/routes/userRoute.js

app.delete("/user/:userId", async (req, res) => {
  try {
    const { userId } = req.params;
      
    // userId가 ObjectId에 맞는 형식인지 유효성 검사
    if (!mongoose.isValidObjectId(userId)) {
      return res.status(400).send({ error: "Invalid userId" });
    }

    // 삭제
    const user = await User.findOneAndDelete({ _id: userId });
    return res.send({ user });

  } catch (error) {
    console.log(error);
    return res.status(500).send({ error: error.message });
  }
});
  • Model.delete({필터 조건}): 해당 조건에 맞는 도큐먼트 삭제
  • Model.findOneAndDelete({필터 조건}): 해당 조건에 맞는 도큐먼트가 있는지 확인 후 삭제
  • Model.findByIdAndDelete(id): 해당 id를 가진 도큐먼트를 삭제


:white_check_mark: update, findOneAndUpdate, findByIdAndUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/routes/userRoute.js

app.put("/user/:userId", async (req, res) => {
  try {
    const { userId } = req.params;

    // userId가 ObjectId에 맞는 형식인지 유효성 검사
    if (!mongoose.isValidObjectId(userId)) 
      return res.status(400).send({ error: "Invalid userId" });
    }

    // 입력한 데이터 유효성 검사
    const { age } = req.body;
    if (!age) return res.status(400).send({ error: "age is required" });
    if (typeof age !== "number") return res.status(400).send({ error: "age must be a number" });
    
    // 해당 id를 가진 데이터의 age를 수정한다.
    // new: true 옵션을 사용하여 업데이트 된 데이터를 반환한다. 
    const user = await User.findByIdAndUpdate(userId, { age }, {new: true});
    return res.send({ user });

  } catch (error) {
    console.log(error);
    return res.status(500).send({ error: error.message });
  }
});
  • Model.update({필터 조건}, {수정하는 값}): 해당 조건에 맞는 도큐먼트 수정
  • Model.findOneAndUpdate({필터 조건}, {수정하는 값}): 해당 조건에 맞는 도큐먼트가 있는지 확인 후 수정
  • Model.findByIdAndUpdate(id, {수정하는 값}): 해당 id를 가진 도큐먼트 수정


그러나 위와 같이 DB를 직접 update하는 것은 schema를 체크하지 않기 때문에 위험할 수 있다.

대신 유저 객체를 찾고 수정한 뒤 저장하면, 데이터 베이스를 두 번 거쳐야 하지만 안정적이게 데이터를 수정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 해당 id를 가진 데이터의 age, name을 수정한다.

// 1. 업데이트 방식
let updateBody = {};
if (age) updateBody.age = age
if (name) updateBody.name = name
 const user = await User.findByIdAndUpdate(userId, updateBody, { new: true });

// 2. save 방식 => user.save할 때 스케마를 체크하기 때문에 좀 더 안정적이다.  
// 수정해야 하는 데이터의 형태가 복잡할 때 추천하는 방식이다. 
let user = await User.findById(userId);
if (age) user.ane = age;
if (name) user.name = name;
await user.save();


This post is licensed under CC BY 4.0 by the author.