상세 컨텐츠

본문 제목

[노드 1팀] 8장. 몽고디비

24-25/Node.js 1

by gooroominuna 2024. 12. 27. 10:00

본문

728x90

 

8.1. SQL vs. No SQL

SQL(MySQL)NoSQL(몽고디비)
규칙에 맞는 데이터 입력
테이블 간 JOIN 지원
안정성, 일관성
용어(테이블, 로우, 컬럼)
자유로운 데이터 입력
컬렉션 간 JOIN 미지원
확장성, 가용성
용어(컬렉션, 다큐먼트, 필드)

 
MySQL과 달리 몽고디비는 컬럼과 자료형, 옵션 등을 정의하지 않고 users 컬렉션을 만들면 끝이다. 또한 몽고디비에는 JOIN 기능이 없다. 
 
📌 몽고디비를 사용하는 이유는 확장성과 가용성 때문이다.
      데이터를 빠르게 넣을 수 있고, 쉽게 여러 서버에 데이터를 분산할 수 있다.
 
애플리케이션을 만들 때 SQL, NoSQL을 동시에 사용할 수 있다. 예를 들어 항공사 예약 시스템의 경우 비행기 표에 관한 정보가 모든 항공사에 일관성 있게 전달되어야 하므로 예약 처리 부분의 데이터베이스는 MySQL을 사용한다. 대신 핵심 기능 외의 빅데이터, 메시징, 세션 관리 등에는 몽고디비를 사용할 수도 있다.
 
 

8.2. 몽고디비 설치하기

몽고디비는 아래 링크에서 다운 받을 수 있다.
https://www.mongodb.com/try/download/community

Try MongoDB Community Edition

Try MongoDB Community Edition on premise non-relational database including the Community Server and Community Kubernetes Operator for your next big project!

www.mongodb.com

 
몽고디비 실행 후 몽고디비 셸을 아래 링크에서 다운 받아 실행한다.
https://www.mongodb.com/try/download/shell

Try MongoDB Tools - Download Free Here

Free download for MongoDB tools to do more with your database. MongoDB Shell, Compass, CLI for Cloud, BI Connector and other database tools available.

www.mongodb.com

 
 

8.4. 데이터베이스 및 컬렉션 생성하기

몽고디비를 설치했다면 몽고디비 프롬프트에 접속 후 데이터베이스와 컬렉션을 만들어보자.
 
📌 데이터베이스 만들기 : use [데이터베이스명]

> use nodejs
switched to db nodejs

 
📌 데이터베이스 목록 확인하기 : show dbs 

> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB

 
📌 현재 사용 중인 데이터베이스 확인하기 : db

> db
nodejs

 
📌 컬렉션 생성하기 : db.createCollection (다큐먼트 넣으면 컬렉션도 자동 생성되므로 따로 생성할 필요 X)

> db.createCollection('users')
{ "ok" : 1 }
> db.createCollection('comments')
{ "ok" : 1 }

 
📌 생성한 컬렉션 목록 확인하기 : show collections

> show collections
comments
users

 
 

8.5. CRUD 작업하기

8.5.1. Create

 

 몽고디비의 자료형

 : 기본적으로 몽고디비는 자바스크립트의 자료형을 따른다. 
   + Date, 정규표현식 같은 자바스크립트 객체와 Binary Data, ObjectId, Int, Long 등의 추가적인 자료형이 있다.
 
 
몽고디비 프롬프트 실행 후 다음 명령어를 입력하자. 

 

📌 다큐먼트 생성 : db.컬렉션명.insertOne(다큐먼트) 

$ mongosh
test> use nodejs;
switched to db nodejs
nodejs> db.users.insertOne({ name: 'zero', age: 24, married: false, comment: '안녕하세요. 간단히 몽고디비 사용 방법에 대해 알아봅시다.', createdAt: new Date() });
{
  acknowledged: true,
  insertedId: ObjectId("5a1687007af03c3700826f70")
}
nodejs> db.users.insertOne({ name: 'nero', age: 32, married: true, comment: '안녕하세요. zero 친구 nero입니다.', createdAt: new Date() });
{
  acknowledged: true,
  insertedId: ObjectId("62fba0deb068d84d69d7c740")
}

 
zero의 아이디 확인 후, 아래와 같이 comments 컬렉션에 데이터를 넣을 수 있다.

nodejs> db.comments.insertOne({ commenter: ObjectId('5a1687007af03c3700826f70'), comment: '안녕하세요. zero의 댓글입니다.', createdAt: new Date() });
{
  acknowledged: true,
  insertedId: ObjectId("62fba1b6b068d84d69d7c741")
}

 

8.5.2. Read

방금 전에 생성한 다큐먼트들을 조회해보자.
 
📌 다큐먼트 조회 : db.users.find({})

nodejs> db.users.find({});
[
  { "_id" : ObjectId("5a1687007af03c3700826f70"), "name" : "zero", "age" : 24, "married" : false, "comment" : "안녕하세요. 간단히 몽고디비 사용 방법을 알아봅시다.", "createdAt" : ISODate("2022-04-30T05:00:00Z") },
  { "_id" : ObjectId("5a16877b7af03c3700826f71"), "name" : "nero", "age" : 32, "married" : true, "comment" : "안녕하세요. zero 친구 nero입니다.", "createdAt" : ISODate("2017-11-23T01:00:00Z") }
]
nodejs> db.comments.find({})
[ { "_id" : ObjectId("5a1687e67af03c3700826f73"), "commenter" : ObjectId("5a1687007af03c3700826f70"), "comment" : "안녕하세요. zero의 댓글입니다.", "createdAt" : ISODate("2022-04-30T05:30:00Z") } ]

 
📌 특정 필드만 조회 : db.users.find({}, { 필드 내용 })
      1 또는  true로 표시한 필드만 조회한다. 
      _id는 기본적으로 가져오므로 0 또는 false를 입력해주어야 한다.

db.users.find({}, { _id: 0, name: 1, married: 1 });

 
  📌 조회 시 조건 주기 : 첫 번째 인수 객체에 조건 기입

nodejs> db.users.find({ age: { $gt: 0 }, married: true }, { _id: 0, name: 1, age: 1 });
[ { "name" : "nero", "age" : 32 } ]
//  age가 30초과, married가 true인 다큐먼트의 이름 및 나이 조회

 
 
→ 몽고디비는 js 객체를 사용해 쿼리를 생성하므로 특수한 연산자가 사용된다.
 
* 연산자
  : $gt(초과), $gte(이상), $lt(미만), $lte(이하), $ne(같지 않음), $or(또는), $in(배열 요소 중 하나) 등
 
  📌 정렬하여 조회 : sort 메서드

nodejs> db.users.find({}, { _id: 0, name:f, age: 1}).sort({ age: -1 })
[
  { "name" : "nero", "age" : 32 },
  { "name" : "zero", "age" : 24 }
]

 
📌 조회할 다큐먼트 개수 설정 : limit 메서드

nodejs> db.users.find({}, { _id: 0, name: 1, age: 1 }).sort({ age: -1 }).limit(1)
[ { "name" : "nero", "age" : 32 } ]

 
📌 건너뛸 다큐먼트 설정 : skip 메서드

nodejs> db.users.find({}, { _id: 0, name: 1, age: 1 }).sort({ age: -1 }).limit(1).skip(1)
[ { "name" : "zero", "age" : 24 } ]

 
이외에도 여러 쿼리가 있다.
 

8.5.3. Update

📌 데이터 수정하기 : db.users.updateOne({ 수정할 다큐먼트 지정 }, { 수정할 내용 })
-> 성공시 첫 번째 객체에 해당하는 다큐먼트 수와 수정된 다큐먼트 수가 반환된다.

nodejs> db.users.updateOne({ name: 'nero' }, { $set: { comment: '안녕하세요 이 필드를 바꿔보겠습니다!' } });
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 0,
  upsertedCount: 0
}

 
 
 
* $set : 어떤 필드를 수정할지 정한다. -> 일부 필드만 수정하고 싶을 때 지정해야 한다.
 

8.5.4. Delete

📌 데이터 삭제 :  db.users.deleteOne({})
-> 성공 시 삭제 된 개수가 반환된다.

nodejs> db.users.deleteOne({ name: 'nero' })
{ acknowledged: true, deletedCount: 1 }

 

8.6. 몽구스 사용하기

 

 몽구스 

MySQL에 시퀄라이즈가 있다면 몽고디비에는 몽구스가 있다.
몽고디비는 다큐먼트를 사용하므로 몽구스는 ORM이 아닌 ODM이라고 불린다.
 

 몽구스의 특징 

  • 스키마 기반 데이터 모델 정의 가능
  • 프로미스 문법, 가독성 높은 쿼리 빌더 지원
  • JOIN 기능을 populate 메서드로 보완 

DB 작업을 직관적으로 처리할 수 있고, 복잡한 쿼리를 간단하게 구성할 수 있다.
 
아래 명령들을 통해 몽구스와 필요한 패키지를 설치해보자.

$ npm i express morgan nunjucks mongoose
$ npm i -D nodemon

 
 
 
 

8.6.1. 몽고디비 연결하기

몽고디비는 주소를 사용해 연결한다.
* 주소 형식 : mongodb://[username:password@]host[:port][/[database][?options]]
 
schemas 폴더 생성, index.js 파일 생성 후 아래와 같이 내용을 넣어보자.

const mongoose = require('mongoose');

const connect = () => {

// 개발 환경일 때만 콘솔을 통해 몽구스가 생성하는 쿼리 확인 가능
 if (process.env.NODE_ENV !== 'production') {
    mongoose.set('debug', true);
  }
  
 // 몽구스와 몽고디비 연결, 콜백 함수를 통해 연결 여부 확인 
 // ** nodejsbook 비밀번호 부분은 바꾸기
  mongoose.connect('mongodb://root:nodejsbook@localhost:27017/admin', {
    dbName: 'nodejs',
    useNewUrlParser: true,
  }, (error) => {
    if (error) {
      console.log('몽고디비 연결 에러', error);
    } else {
      console.log('몽고디비 연결 성공');
    }
  });
};

// 에러 발생 시 에러 내용 기록, 연결 종료 시 재연결 시도
mongoose.connection.on('error', (error) => {
  console.error('몽고디비 연결 에러', error);
});
mongoose.connection.on('disconnected', () => {
  console.error('몽고디비 연결이 끊겼습니다. 연결을 재시도합니다.');
  connect();
});
  
module.exports = connect;

 
이제 아래 코드로 app.js를 만들고 schemas/index.js와 연결해보자.

const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');

const connect = require('./schemas');

const app = express();
app.set('port', process.env.PORT || 3002);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});
connect();

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
});

 
몽고디비 서버를 실행한 후, npm start 명령어로 웹 서버를 실행해보자.
에러는 1) 몽고디비 데이터베이스를 실행하지 않은 경우 2) 비밀번호가 틀린 경우에 발생한다.
 

8.6.2. 스키마 정의하기

schemas 폴더에 user.js, comment.js를 만들어보자.
먼저 사용자 스키마를 정의해보자.

// user.js
const mongoose = require('mongoose');

const { Schema } = mongoose;
const userSchema = new Schema({
  name: {
    type: String,
    required: true,
    unique: true,
  },
  age: {
    type: Number,
    required: true,
  },
  married: {
    type: Boolean,
    required: true,
  },
  comment: String,
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model('User', userSchema); // 스키마와 몽고디비 컬렉션 연결하는 모델 생성

 
댓글 스키마를 만들어보자.

// comment.js
const mongoose = require('mongoose');

const { Schema } = mongoose;
const { Types: { ObjectId } } = Schema;
const commentSchema = new Schema({
  commenter: { // JOIN과 비슷한 기능할 때 사용됨
    type: ObjectId,
    required: true,
    ref: 'User',
  },
  comment: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model('Comment', commentSchema);

 

8.6.3. 쿼리 수행하기

몽구스를 사용해 쿼리를 수행해보자.

// mongoose.js
// 사용자 이름을 눌렀을 때 댓글 로딩
document.querySelectorAll('#user-list tr').forEach((el) => {
  el.addEventListener('click', function () {
    const id = el.querySelector('td').textContent;
    getComment(id);
  });
});
// 사용자 로딩
async function getUser() {
  try {
    const res = await axios.get('/users');
    const users = res.data;
    console.log(users);
    const tbody = document.querySelector('#user-list tbody');
    tbody.innerHTML = '';
    users.map(function (user) {
      const row = document.createElement('tr');
      row.addEventListener('click', () => {
        getComment(user._id);
      });
      // 로우 셀 추가
      let td = document.createElement('td');
      td.textContent = user._id;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = user.name;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = user.age;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = user.married ? '기혼' : '미혼';
      row.appendChild(td);
      tbody.appendChild(row);
    });
  } catch (err) {
    console.error(err);
  }
}
// 댓글 로딩
async function getComment(id) {
  try {
    const res = await axios.get(`/users/${id}/comments`);
    const comments = res.data;
    const tbody = document.querySelector('#comment-list tbody');
    tbody.innerHTML = '';
    comments.map(function (comment) {
      // 로우 셀 추가
      const row = document.createElement('tr');
      let td = document.createElement('td');
      td.textContent = comment._id;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = comment.commenter.name;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = comment.comment;
      row.appendChild(td);
      const edit = document.createElement('button');
      edit.textContent = '수정';
      edit.addEventListener('click', async () => { // 수정 클릭 시
        const newComment = prompt('바꿀 내용을 입력하세요');
        if (!newComment) {
          return alert('내용을 반드시 입력하셔야 합니다');
        }
        try {
          await axios.patch(`/comments/${comment._id}`, { comment: newComment });
          getComment(id);
        } catch (err) {
          console.error(err);
        }
      });
      const remove = document.createElement('button');
      remove.textContent = '삭제';
      remove.addEventListener('click', async () => { // 삭제 클릭 시
        try {
          await axios.delete(`/comments/${comment._id}`);
          getComment(id);
        } catch (err) {
          console.error(err);
        }
      });
      // 버튼 추가
      td = document.createElement('td');
      td.appendChild(edit);
      row.appendChild(td);
      td = document.createElement('td');
      td.appendChild(remove);
      row.appendChild(td);
      tbody.appendChild(row);
    });
  } catch (err) {
    console.error(err);
  }
}
// 사용자 등록 시
document.getElementById('user-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const name = e.target.username.value;
  const age = e.target.age.value;
  const married = e.target.married.checked;
  if (!name) {
    return alert('이름을 입력하세요');
  }
  if (!age) {
    return alert('나이를 입력하세요');
  }
  try {
    await axios.post('/users', { name, age, married });
    getUser();
  } catch (err) {
    console.error(err);
  }
  e.target.username.value = '';
  e.target.age.value = '';
  e.target.married.checked = false;
});
// 댓글 등록 시
document.getElementById('comment-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const id = e.target.userid.value;
  const comment = e.target.comment.value;
  if (!id) {
    return alert('아이디를 입력하세요');
  }
  if (!comment) {
    return alert('댓글을 입력하세요');
  }
  try {
    await axios.post('/comments', { id, comment });
    getComment(id);
  } catch (err) {
    console.error(err);
  }
  e.target.userid.value = '';
  e.target.comment.value = '';
});

 
뒤에서 만들 라우터들을 미리 app.js에 연결한다.

// app.js
...
const connect = require('./schemas');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const commentsRouter = require('./routes/comments');

const app = express();
...
app.use(express.urlencoded({ extended: false }));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comments', commentsRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
...

 
이제 라우터를 만들어보자.

// routes/index.js
const express = require('express');
const User = require('../schemas/user');

const router = express.Router();

router.get('/', async (req, res, next) => {
  try {
    const users = await User.find({});
    res.render('mongoose', { users });
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

 
index.js는 GET /로 접속한 경우의 라우터이다.
몽구스도 프로미스를 지원하므로 async/await과 try/catch문을 사용해 조회 성공/실패 시의 정보를 얻을 수 있다.
 

// users.js
const express = require('express');
const User = require('../schemas/user');
const Comment = require('../schemas/comment');

const router = express.Router();

router.route('/')
  .get(async (req, res, next) => {
    try {
      const users = await User.find({});
      res.json(users);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .post(async (req, res, next) => {
    try {
      const user = await User.create({
        name: req.body.name,
        age: req.body.age,
        married: req.body.married,
      });
      console.log(user);
      res.status(201).json(user);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

router.get('/:id/comments', async (req, res, next) => {
  try {
    const comments = await Comment.find({ commenter: req.params.id })
      .populate('commenter');
    console.log(comments);
    res.json(comments);
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

 
📌 users.js는 *GET /users POST /users 주소로 요청이 들어올 때의 라우터이다.
   사용자 조회, 사용자 등록 요청을 처리한다.
* GET /users에서는 데이터를 JSON 형식으로 반환한다.
 
위 코드의 GET /users/:id/comments 라우터는 댓글 다큐먼트를 조회한다.
1) find 메서드 댓글 쓴 사용자 아이디로 댓글 조회
2) popular 메서드 관련 있는 컬렉션의 다큐먼트 불러옴 
 
이제 댓글에 관련된 CRUD 작업을 하는 라우터를 만들어보자.

// comments.js
const express = require('express');
const Comment = require('../schemas/comment');

const router = express.Router();

router.post('/', async (req, res, next) => {
  try {
    const comment = await Comment.create({
      commenter: req.body.id,
      comment: req.body.comment,
    });
    console.log(comment);
    const result = await Comment.populate(comment, { path: 'commenter' });
    res.status(201).json(result);
  } catch (err) {
    console.error(err);
    next(err);
  }
});

router.route('/:id')
  .patch(async (req, res, next) => {
    try {
      const result = await Comment.update({
        _id: req.params.id,
      }, {
        comment: req.body.comment,
      });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .delete(async (req, res, next) => {
    try {
      const result = await Comment.remove({ _id: req.params.id });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

module.exports = router;

 
- POST /comments 라우터 : 다큐먼트 등록
- PATCH /comments/:id 라우터 : 다큐먼트 수정
- DELETE /comments/:id 라우터 : 다큐먼트 삭제
 
 
이제 npm start로 웹 서버를 실행하고 웹 사이트에 접속해보면 아래와 같이 나온다.
 

 


Quiz

1. 몽고디비는 대표적으로 MySQL과 달리 ( 조인 ) 기능을 지원하지 않는다.
2. 몽고디비는 기본적으로 ( 자바스크립트 )의 자료형을 따른다.
3. 몽고디비에서 컬렉션 내의 모든 다큐먼트를 조회하려면 ( find({}) )를 사용한다.
4. 몽고디비에서 기존 데이터 1개를 수정할 때 ( updateOne({}, {}) )를 사용한다.
5. 몽고디비에서 기존 데이터 1개를 삭제할 때 ( deleteOne({}) )를 사용한다.
6. 몽구스는 다큐먼트를 사용하므로 ORM이 아닌 ( ODM ) 이다.
7. 몽고디비는 ( 주소 )를 사용해 연결한다.
 

Programming Quiz

1. 몽구스 모듈에서 Schema 생성자를 사용해 스키마를 만드시오. (name과 age 필드만 정의)
2. GET /로 접속했을 때의 라우터 코드를 작성하시오.
 


Answer

1. 

const mongoose = require('mongoose');

const { Schema } = mongoose;
const userSchema = new Schema({
  name: {
    type: String,
    required: true,
    unique: true,
  },
  age: {
    type: Number,
    required: true,
  },
});

module.exports = mongoose.model('User', userSchema);

 
2.

const express = require('express');
const User = require('../schemas/user');

const router = express.Router();

router.get('/', async (req, res, next) => {
  try {
    const users = await User.find({});
    res.render('mongoose', { users });
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

 


출처 :  조현영,  Node.js 교과서 개정 3판, 길벗(2022)

Corner Node.js 1
Editor : Oze

 

 

728x90

관련글 더보기