Notice
Recent Posts
Recent Comments
관리 메뉴

즐겁게, 코드

Express.js & MongoDB 기반 REST api 구현하기 - 3편 본문

💻 백엔드/Node.js

Express.js & MongoDB 기반 REST api 구현하기 - 3편

Chamming2 2021. 1. 2. 01:53

[지난 글 목록]

Express.js & MongoDB 기반 REST api 구현하기 - 1편 (서버 만들기)

Express.js & MongoDB 기반 REST api 구현하기 - 2편 (몽고디비 연결하기)

 

이전 단계에서 몽고디비(MongoDB)를 사용할 준비를 마쳤으니, 이제 몽고디비를 사용해 보자.

몽고디비는 행렬 테이블로 이루어진 기존의 관계형 데이터베이스와는 다르게 어떤 값이든 DB에 어떤 형태로든 추가될 수 있다.

 

이는 NoSQL 방식의 큰 장점이기도 하지만 자칫하면 무질서한 데이터들로 인해 DB의 무결성을 해칠 수도 있다.

그래서 몽구스에서 제공하는 것이 스키마(Schema)로, 스키마는 데이터 포맷을 일종의 템플릿으로 정해줌으로써 몽고디비에서도 MySQL처럼 구조화된 데이터를 관리할 수 있게 된다.

 

Ex. [MySQL 레코드 형식]

  name | phone | email 
  찬민    7477    chanmin@gmail.com
  수민    2544    soovely@gmail.com

Ex. [Mongoose 스키마 형식]

const mongoose = require("mongoose");

const personSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  phone: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
    default: "",
  },
});
// 몽고디비에 실제로 저장되는 개별 데이터는 '다큐먼트' 라고 부른다.
[
  {
      "_id": "5fef0e6819c5103420c38ef3",
      "name": "찬민",
      "phone": "7477",
      "email": "chanmin@gmail.com"
  },
  {
      "_id": "9371f8c91280c91ca38ef259",
      "name": "수민",
      "phone": "2544",
      "email": "soovely@gmail.com"
  }
]

이제 서비스의 계정 정보를 제어한다고 생각하고 스키마를 별도의 파일로 분리한다.

스키마는 자바스크립트 객체와 동일한 문법을 사용하므로 매우 친숙하게 느껴질 것이다.

 

[스키마 항목 속성]

- type: 데이터의 형식(타입)을 지정한다. Number, Boolean 등의 원시 타입은 자바스크립트의 원시형과 완전히 동일하다.

다만 몽구스에서만 지원하는 몇 가지 타입이 더 존재하는데 이는 Mongoose 공식 문서 - 스키마 타입 에서 찾아볼 수 있다. 

- required: 해당 속성의 필수여부를 지정한다.

- default: 값이 주어지지 않을 때 사용할 초기값을 지정한다.

 

이를 토대로 스키마를 완성하면 외부 파일에서 스키마를 사용할 수 있도록 mongoose.model() 을 통해 내보낸다.

이 때 mongoose.model() 의 첫 번째 인자는 컬렉션 이름, 두 번째 인자는 내보낼 스키마의 이름이다.

// accountModel.js
const mongoose = require("mongoose");

const accountSchema = new mongoose.Schema({
  nickname: {
    type: String,
    required: true,
  },
  major: {
    type: String,
    required: true,
  },
  created_at: {
    type: Date,
    required: true,
    default: Date.now,
  },
});

module.exports = mongoose.model("Account", accountSchema);

위처럼 닉네임과 전공으로 구성된 기본적인 스키마를 완성했다.

 

본격적인 코드를 설명하기 전에 사용할 용어를 통일하자면 다음과 같다.

- 스키마 : 데이터의 틀 (클래스)

- 모델 : 스키마를 통해 생성된 객체 (객체)

- 다큐먼트 : 몽고디비 상에서 존재하는 각 데이터

 

이제 REST api로 몽고디비를 조작하기 위해 라우터 파일에서 스키마를 불러온 후 api별로 수행할 동작을 채워보자.

// routes.js
const express = require("express");
const router = express.Router();
const Account = require("./accountModel");

router.get("/", (req, res) => {
  res.send("GET 요청을 수신했습니다.");
});

router.post("/", (req, res) => {
  res.send("POST 요청을 수신했습니다.");
})

router.patch("/:id", (req, res) => {
  res.send("PATCH 요청을 수신했습니다.");
})

router.delete("/:id", (req, res) => {
  res.send("DELETE 요청을 수신했습니다.");
})

module.exports = router;

[1. 모든 계정 데이터 불러오기 (GET 요청 시)]

const express = require("express");
const router = express.Router();
const Account = require("./accountModel");

router.get("/", async (req, res) => {
  try {
    const accounts = await Account.find();
    res.json(accounts);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

(스키마).find() 는 해당 스키마로 작성된 모든 다큐먼트의 배열을 리턴한다.

데이터를 불러오는데 성공하면 이를 클라이언트에 응답으로 전송하고, 오류가 발생하면 에러 메시지를 전송한다.

 

[2. 계정 데이터 추가하기 (POST 요청 시)]

const express = require("express");
const router = express.Router();
const Account = require("./accountModel");

router.post("/", async (req, res) => {
  const account = new Account({
    nickname: req.body.nickname,
    major: req.body.major,
  });
  
  try {
    const newAccount = await account.save();
    res.status(201).json(newAccount);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

요청 본문에 포함된 별명과 전공 정보를 기반으로 모델(account)을 생성한다.

이후 (모델).save() 를 통해 이를 저장하고 몽고디비의 다큐먼트로 추가한다.

 

[3. 특정 id를 갖는 다큐먼트 찾기]

갱신과 삭제는 주어진 id를 갖는 다큐먼트를 지목해야 하므로 이를 위한 미들웨어도 작성해준다.

async function getAccount(req, res, next) {
  let account;
  try {
    account = await Account.findById(req.params.id);
    if (account == null) {
      return res.status(404).json({ message: "계정을 찾을 수 없습니다." });
    }
  } catch (err) {
    return res.status(500).json({ message: err.message });
  }

  res.account = account;
  next();
}

(스키마).findById()는 인자와 일치하는 id를 갖는 다큐먼트를 반환한다.  

 

[4. 계정 데이터 갱신하기 (PATCH 요청 시)]

const express = require("express");
const router = express.Router();
const Account = require("./accountModel");

router.patch("/:id", getAccount, async (req, res) => {
  if (req.body.nickname != null) {
    res.account.nickname = req.body.nickname;
  }
  if (req.body.major != null) {
    res.account.major = req.body.major;
  }
  try {
    const updatedAccount = await res.account.save();
    res.json(updatedAccount);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

위 코드는 요청 본문으로 받은 닉네임과 전공을 특정 큐먼트에 덮어쓴다.

 

[5. 계정 데이터 삭제하기 (DELETE 요청 시)]

const express = require("express");
const router = express.Router();
const Account = require("./accountModel");

router.delete("/:id", getAccount, async (req, res) => {
  try {
    await res.account.remove();
    res.json({ message: "계정 정보를 삭제했습니다." });
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

마지막으로 (모델).remove() 를 통해 다큐먼트를 디비에서 삭제할 수도 있다.

나중을 위해 요약하자면 다음과 같다.

 

스키마.find() -> (읽기) 모든 다큐먼트 목록 리턴

모델.save() -> (쓰기) 디비에 다큐먼트 추가

모델.remove() -> (쓰기) 디비에서 다큐먼트 제거

 

하나 눈여겨봐야 할 점은 지금까지 모두 동일한 URI에 대해 요청을 수행했다는 것이다.

 

이제 개발자는 "삭제는 /deleteAccount, 추가는 /createAccount, 업데이트는 어디였지..?" 하고 수많은 URL로 둘러싸여 좌절할 필요 없이 HTTP 메서드만으로 요청에 따른 작업을 수행할 수 있게 되었다.

반응형
Comments
소소한 팁 : 광고를 눌러주시면, 제가 뮤지컬을 마음껏 보러다닐 수 있어요!
와!! 바로 눌러야겠네요! 😆