[Node.js] Database - MongoDB


SQL vs NoSQL

  • 흔히 사용되는 Database는 크게 SQL과 NoSQL 두 가지 타입으로 구분한다.
  • 우리가 어떤 프로젝트를 진행하고 서비스를 만들든, 어떤 DB를 사용해야하는지는 많은 고민을 하게 만든다.
  • 이 두 가지는 반대되는 개념이라기보다는, '다른' 개념이라고 이해하는 것이 좋을 것 같다.
  • 각각의 특징을 살펴보고, 어떤 상황에 어떤 DB를 사용해야할 지를 고려하면 될 것 같다.

(1) SQL

  • Structured Query Language의 약자
  • 데이터가 고정된 열과 행을 가지고 있는 테이블에 저장될 경우, 흔히 SQL 방식이라고 부른다.
  • 관계형 데이터베이스(RDB : Relational DataBase)라고 부르기도 한다.
  • 대표적으로는 MySQL, Oracle, MsSQL 등이 있다.
  • '스키마' 라고 불리는 데이터 테이블의 형태로 관리한다.
  • 각 테이블이 담고있는 정보가 비교적 명확한 편이기 때문에, 관리가 용이하고 이해가 쉬운 편이다.

(2) NoSQL

  • 관계형 데이터베이스만을 쓰지 않는 방식을 이야기한다.
  • 주로 비관계형 데이터베이스라고 하지만, 관계형 데이터베이스 시스템의 방식을 전혀 쓰지 못하는건 아니다.
  • SQL의 '스키마' 처럼 정해진 형태가 있는게 아니기 때문에, 데이터를 유동적으로 변경하고 관리할 수 있다.
  • 하지만 이 때문에 데이터가 복잡해질 수도 있고, SQL에 비해 데이터베이스에 대한 이해가 어려워질 수 있다.
  • 서비스를 유동적으로 변경해야하는 스타트업에서 이 방식을 주로 선호한다.
  • 대표적으로는 MongoDB, Hadoop 이 있다.
  • 데이터가 주로 Json 형태 {'key' : 'value'} 로 이루어져있다.

MongoDB

  • MonboDB는 대표적인 NoSQL 중 하나이다.
  • 간단한 프로젝트를 통해, MongoDB의 사용법을 익혀본다.
  • MongoDB를 사용하기 위해, MongoDB, Robo3T를 설치하고 Mongoose 라이브러리를 사용할 예정이다.

(1) MongoDB 설치 (Mac)

  • 우선, Terminal을 실행한 후, 아래 명령어를 통해 MongoDB를 설치한다.
// 커스텀 홈브루 탭 세팅
brew tap mongodb/brew

// MongoDB 설치
brew install mongodb-community

// MongoDB 실행
brew services start mongodb-community
  • 인터넷 브라우저를 켜고 http://localhost:27017 주소로 접속했을 때 아래와 같은 화면이 보인다면 정상적으로 설치와 실행이 완료된 것이다.

(2) Robo 3T 설치

Robo 3T는 MongoDB Database를 직관적으로 확인하고 조작할 수 있는 GUI 프로그램이다.

  • 위 화면의 좌측 상단, 컴퓨터 모양 아이콘을 누르면 MongoDB Connections 창이 뜬다.
  • Create를 클릭한 후, 'Name'에 원하는 Connection 이름을 작성한다.
  • 현재는 Local 환경에 Database를 구축할 것이기 때문에, 주소와 포트는 그대로 둔다.

  • 원하는 이름으로 Connection을 생성한 후 Save를 누르면, 위와 같은 화면을 볼 수 있다.
  • 이제, 내 Local 환경에서 MongoDB를 통해 데이터를 관리할 준비가 되었다.

(3) Mongoose 설치

  • Mongoose는 Node.js 에서 MongoDB 연동 라이브러리 중, 가장 많이 사용되는 것이다.
  • NoSQL에는 '스키마' 라는 개념이 없지만, RDB에서 처럼 '스키마' 를 사용할 수 있도록 해준다.
  • 설치는, npm을 통해서 진행하면 아주 간단하게 완료할 수 있다.
  • 우선, 작성중인 Node.js 프로젝트에서 아래 명령어를 실행한다.
npm i mongoose
  • 이제 MongoDB를 사용할 준비가 끝났다.

Mongoose로 데이터 다루기

Mongoose를 통해, MongoDB의 CRUD 기능을 다루어본다.

(1) Database module 생성하기

  1. Mongoose 라이브러리를 사용하기 위해 require()를 통해 불러온다.
  2. 위에서 생성한 MongoDB Connection에 접속한다.
  3. 사용할 Database 이름을 정해준다.
  4. 외부에서 사용하기 위해 Module을 내보낸다.
  • 위 순서대로 간단하게 진행해보도록 하겠다.
  • 우선, 프로젝트에서 'schemas' 폴더를 만들고 그 안에 index.js 파일을 생성했다.
// ./schemas/index.js

// mongoose 라이브러리 불러오기
const mongoose = require('mongoose');
// connect 객체 생성
// connect 주소는 항상 mongodb://~~~ 의 형식이어야 한다. 
// 주소 뒤의 'mydb' 부분에, Database 이름을 작성해주면 된다. 
const connect = () => {
    mongoose
        .connect('mongodb://localhost:27017/mydb', {
            ignoreUndefined: true,
        })
        .catch(err => console.log(err));
};

// 외부에서 사용하기 위해, 모듈을 내보낸다.
module.exports = connect;

(2) Database 연결하기

  • 실행하고자 하는 파일은 app.js 파일이므로, 이 파일에서 모듈을 사용해보도록 한다.
  • Express와 함께 사용하면 아래와 같다.
// express
const express = require('express');
const app = express();
const port = 8080;

// mongoose
// index.js 파일은 이름을 생략할 수 있기 때문에, ./schemas 만 입력
const connect = require('./schemas');
// connect mongoose
connect();
  • 위와 같이 코드를 작성하고 app.js를 통해 실행하면, ./schema/index.js 파일에서 작성한 내용대로 MongoDB 서버에 연결이 정상적으로 이루어진다.

(3) Schema, Model 만들기

  • Mongoose는 RDB처럼 Schema를 만들 수 있다고 했는데, 어떻게 만들 수 있는지 살펴본다.
  • schemas 폴더에 'goods.js' 파일을 만들고 아래와 같이 코드를 작성한다.
const mongoose = require('mongoose');

const goodsSchema = new mongoose.Schema({
    goodsId: {
        type: Number,
        required: true,
        unique: true,
    },
    name: {
        type: String,
        required: true,
        unique: true,
    },
    thumbnailUrl: {
        type: String,
    },
    category: {
        type: String,
    },
    price: {
        type: Number,
    },
});

// mongoose.model() 의 첫번째 인자 'Goods'가 Collection의 이름이 되는 것이다.
module.exports = mongoose.model('Goods', goodsSchema);
  • 아주 직관적이기 때문에, 크게 설명할 것이 없어보인다.
  • required는 필수 값, unique는 중복되지 않는 값이어야 한다는 의미이다.
  • 마지막 줄의 코드에서 보면 'model'이라는 메서드를 사용했는데, 여기서 model은 우리의 데이터가 어떻게 생겼는지를 Database Collection에 정의하고, JavaScript와 서로 상호작용할 수 있도록 해주는 객체라고 보면 될 것 같다. (여기서 Database Collection은 RDB에서의 Table이라고 보면 되겠다.)
  • 즉, 이 model 객체를 이용해서 DB의 해당 Collection에 데이터를 넣고, 수정하고, 삭제할 수 있다.

(4) Router와 연결시켜주기

  • 이전 프로젝트에서 Router를 사용했기 때문에, DB에 접근하는 것 역시 이 Router를 통해 이루어질 것이다. 따라서, 우리가 생성해준 Router 파일에서 사용할 수 있도록 코드를 작성해준다.
// ./routes/goods.js

const express = require('express');
const router = express.Router();

// goods에서 exports 한 model 객체를 그대로 'Goods' 변수에 할당한다.
const Goods = require('../schemas/goods');
  • 위와 같이 require() 를 사용해서 Model을 받아오면, 바로 사용이 가능하다.

(5) CRUD 구현해보기

  • 드디어, DB에 데이터를 읽고, 쓰고, 수정하고, 삭제할 수 있는 상태가 되었다.
  • 아래 코드를 통해 간략하게 CRUD 방법을 살펴본다.
// ./routes/goods.js

const express = require('express');
const router = express.Router();
const Goods = require('../schemas/goods');

// 상품 추가 (Create)
router.post('/goods/:goodsId/cart', async (req, res) => {
    const { goodsId } = req.params;
    const { quantity } = req.body;

    // 유의사항: .find()는 Promise 객체를 반환하기 때문에, await/async를 사용한다.
    const existsCarts = await Cart.find({ goodsId: Number(goodsId) });
    if (existsCarts.length) {
        return res.status(400).json({
            success: false,
            errorMessage: '이미 장바구니에 있는 상품입니다.',
        });
    }

    await Cart.create({ goodsId: Number(goodsId), quantity });
    res.json({ success: true });
});

// 전체 목록 조회 (Read)
router.get('/goods', async (req, res) => {
    const { category } = req.query;
    console.log('category?', category);

  	// 유의사항: .find()는 Promise 객체를 반환하기 때문에, await/async를 사용한다.
    const goods = await Goods.find({ category });
    res.json({
        // 객체의 이름이 key와 똑같다면, 약식으로 하나만 써주어도 된다.
        // 아래 코드는 goods: goods 와 동일하다.
        goods,
    });
});

// 상품 수량 수정 (Update)
router.put('/goods/:goodsId/cart', async (req, res) => {
    const { goodsId } = req.params;
    const { quantity } = req.body;
    console.log(`goodsId: ${goodsId}`);
    console.log(`quantity: ${quantity}`);

    // 유의사항: .find()는 Promise 객체를 반환하기 때문에, await/async를 사용한다.
    const existsCarts = await Cart.find({ goodsId: Number(goodsId) });
    console.log(`existsCarts: ${existsCarts}`);
    if (!existsCarts.length) {
        return res.status(400).json({
            success: false,
            errorMessage: '장바구니에 상품이 없습니다.',
        });
    }

    // 유의사항: .updateOne()은 Promise 객체를 반환하기 때문에, await/async를 사용한다.
    await Cart.updateOne({ goodsId: Number(goodsId) }, { $set: { quantity } });
    res.json({ success: true });
});

// 상품 삭제 (Delete)
router.delete('/goods/:goodsId/cart', async (req, res) => {
    const { goodsId } = req.params;

    // 유의사항: .find()는 Promise 객체를 반환하기 때문에, await/async를 사용한다.
    const existsCarts = await Cart.find({ goodsId: Number(goodsId) });
    if (existsCarts.length) {
        await Cart.deleteOne({ goodsId: Number(goodsId) });
    }

    res.json({ success: true });
});
  • find(), updateOne() 등의 메서드는 Promise 객체를 반환하기 때문에 꼭 async/await 이나 Promise 구문으로 작성해주어야 한다.

좋은 웹페이지 즐겨찾기