8. 몽고디비
포스트
취소

8. 몽고디비

기존 블로그에 작성했던 포스트를 이전한 글입니다.

해당 포스트는 NODEJS를 학습하며 정리한 내용에 대한 포스트입니다.




🌈 몽고디비

💻 NoSQL vs SQL

MySQL같은 SQL 데이터베이스와는 다른 유형의 데이터

  • NoSQL의 대표주자인 mongoDB(몽고디비) 사용

  • JOIN: 관계가 있는 테이블끼리 데이터를 합치는 기능(몽고디비 aggregate로 흉내 가능)

  • 빅데이터, 메시징, 세션 관리 등(비정형 데이터)에는 몽고디비 사용하면 좋음

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




💻 몽고디비 설치하기

몽고디비 공식 사이트에서 내려받을 수 있습니다.


🍳 몽고디비 연결하기

윈도의 경우 C:\에 data 폴더를 만들고 그 안에 db 폴더를 만듦

콘솔로 몽고디비가 설치된 경로(기본적으로 C:\Program Files\MongoDB\Server\6.0\bin)로 이동해 몽고디비를 실행

1
mongod --ipv6

몽고디비를 실행

몽고디비 셸도 설치

mongod 가 실행 중인 상태에서 mongosh 실행

mongosh


🍳 어드민 설정하기

어드민 권한을 설정하여 디비에 비밀번호 걸기

1
2
3
4
test> use admin
switched to db admin
admin> db.createUser({ user: '이름', pwd: '비밀번호', roles: ['root'] })
Successfully added user: { "user" : "root", "roles" : [ "root" ] }

mongod를 입력했던 콘솔을 종료한 후 mongod --ipv6 --auth 명령어로 접속

  • --auth는 로그인이 필요하다는 뜻

mongosh 콘솔도 종료한 후 mongosh admin -u 이름 -p 비밀번호 명령어로 접속

1
2
3
4
5
6
7
$ mongosh admin -u [이름] -p [비밀번호]
Current Mongosh Log ID: 62f9d501a04fc0bcbc7f2791
Connecting to:          mongodb://<credentials>@127.0.0.1:27017/admin?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.5.4
Using MongoDB:          6.0.0
Using Mongosh:          1.5.4
(생략)
admin>

방금 입력한 명령어는 이름과 비밀번호가 잘 생성되었는지 확인하기 위한 것이었으므로, 앞으로 몽고디비 프롬프트를 이용할 때는 단순히 mongosh만 입력하면 된다.




💻 COMPASS

컴퍼스도 몽고디비 공식 사이트에서 다운받을 수 있다.


🍳 커넥션 생성하기

컴퍼스(MongoDB Compass Community)로 접속

Authentication 을 Username/Password로 변경, 몽고디비 계정 이름과 비밀번호 입력

커넥션 생성하기




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

몽고디비 프롬프트에 접속한 후 진행하면 된다.

데이터베이스를 만드는 명령어는 use [데이터베이스명]입니다.

1
2
> use nodejs
switched to db nodejs

데이터베이스 목록을 확인하는 명령어는 show dbs

1
2
3
4
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB

방금 생성한 nodejs가 없지만 당황하지 말자.

데이터를 최소 한 개 이상 넣어야 목록에 표시된다.

현재 사용 중인 데이터베이스를 확인하는 명령어는 db

1
2
> db
nodejs

비록 데이터베이스 목록에는 없지만, 현재 nodejs 데이터베이스를 사용하고 있음을 확인할 수 있다.

컬렉션은 따로 생성할 필요가 없다. 다큐먼트를 넣는 순간 컬렉션도 자동으로 생성된다. 하지만 다음과 같이 직접 컬렉션을 생성하는 명령어가 있다.

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

생성한 컬렉션 목록을 확인 show collections

1
2
3
> show collections
comments
users




💻 CRUD 작업하기

🍳 Create

  • 몽고디비는 컬럼을 정의하지 않아도 됨

  • 자유로움이 장점, 무엇이 들어올지 모른다는 단점

  • 자바스크립트의 자료형을 따름(차이점도 존재)

  • ObjectId: 몽고디비의 자료형으로 고유 아이디 역할을 함

  • save method로 저장

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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")
}

db.컬렉션명.insertOne(다큐먼트)로 다큐먼트를 생성할 수 있다.

자바스크립트 객체처럼 생성하면 된다.


🍳 Create(관계 설정)

컬렉션 간 관계를 강요하는 제한이 없으므로 직접 ObjectId를 넣어 연결

사용자의 ObjectId를 찾은 뒤 댓글 컬렉션에 넣음

image


🍳 Read

find로 모두 조회, findOne으로 하나만 조회

1
2
3
4
5
6
7
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") } ]

find({})는 컬렉션 내의 모든 다큐먼트를 조회하라는 뜻


🍳 Read(조건)

두 번째 인수로 조회할 필드를 선택할 수 있음(1은 추가, 0은 제외)

1
2
3
4
5
nodejs> db.users.find({}, { _id: 0, name: 1, married: 1 });
[
  { "name" : "zero", "married" : false },
  { "name" : "nero", "married" : true }
]

첫 번째 인수로 조회 조건 입력 가능

$gt나 $or같은 조건 연산자 사용

$gt(초과), $gte(이상), $lt(미만), $lte(이하), $ne(같지 않음), $or(또는), $in(배열 요소 중 하나) 등이 있다.

1
2
nodejs> db.users.find({ age: { $gt: 0 }, married: true }, { _id: 0, name: 1, age: 1 });
[ { "name" : "nero", "age" : 32 } ]
1
2
3
4
5
nodejs> db.users.find({ $or: [{ age: { $gt: 30 } }, { married: false }] }, { _id: 0, name: 1, age: 1 });
[
  { "name" : "zero", "age" : 24 },
  { "name" : "nero", "age" : 32 }
]

정렬은 sort 메서드로 함

-1 은 내림차순, 1은 오름차순

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

limit 메서드로 조회할 다큐먼트 개수 제한

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

skip 메서드로 건너뛸 다큐먼트 개수 제공

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


🍳 Update

첫 번째 객체는 수정할 다큐먼트를 지정하는 객체

두 번째 객체는 수정할 내용을 입력하는 객체

$set을 붙이지 않으면 다큐먼트 전체가 대체되므로 주의

하나만 수정할 때 updateOne, 여러 개 수정할 때 updateMany

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


🍳 Delete

하나만 수정할 때 deleteOne, 여러 개 수정할 때 deleteMany

첫 번째 인수로 삭제할 대상 조건 제공

성공 시 삭제된 개수가 반환됨

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




💻 몽구스 사용하기

🍳 몽구스 ODM

몽고디비 작업을 쉽게 할 수 있도록 도와주는 라이브러리

ODM(Object Document Mapping): 객체와 다큐먼트를 매핑(1대1 짝지음)

몽고디비에 없어 불편한 기능들을 몽구스가 보완

테이블과 유사한 기능, JOIN 기능 추가

소스 코드 참고

  • 프로젝트 세팅 후, 콘솔을 통해 경로로 이동한 후 package.json 설치

🔻 pakage.json

1
2
3
4
5
6
7
8
9
10
11
{
  "name": "learn-mongoose",
  "version": "0.0.1",
  "description": "몽구스를 배우자",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "ZeroCho",
  "license": "MIT"
}

설치

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


🍳 몽고디비 연결하기

몽구스를 통해 몽고디비 연결하기

  • 인증은 admin 데이터베이스에서, 서비스는 dbName 데이터베이스에서

🔻scehmas/index.js

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
const mongoose = require('mongoose');

const connect = () => {
  if (process.env.NODE_ENV !== 'production') {
    mongoose.set('debug', true);
  }
  mongoose.connect('mongodb://root:nodejsbook@localhost:27017/admin', {
    dbName: 'nodejs',
    useNewUrlParser: true,
    useCreateIndex: 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의 함수가 실행됨

  • mongoose.connect 함수가 몽고디비에 연결을 시도

  • mongoose.set은 디버깅 모드(모드를 켰을 때 콘솔에 쿼리가 찍힘)

  • 연결이 끊기면(disconnection) 다시 연결을 시도

🔻app.js

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
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'), '번 포트에서 대기 중');
});


🍳 스키마 정의하기

schemas 폴더 안에 작성

  • MySQL의 테이블처럼 정해진 데이터만 들어갈 수 있게 강제함

  • type은 자료형, require는 필수 여부 default는 기본값, unique는 고유 여부

🔻schemas/user.js

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
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);

🔻schemas/comment.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const mongoose = require('mongoose');

const { Schema } = mongoose;
const { Types: { ObjectId } } = Schema;
const commentSchema = new Schema({
  commenter: {
    type: ObjectId,
    required: true,
    ref: 'User',
  },
  comment: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

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


🍳 라우터 작성

프론트엔드 코드는 서버에 요청을 보내는 AJAX 요청 위주로

서버 코드는 응답을 보내는 라우터 위주로 살펴보기

다른 소스 코드 참조 8.6.3

🔻routes.index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;


🍳 사용자 라우터

router.get, post, put, patch, delete 라우터 작성

🔻routes/users.js

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
42
43
44
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;


🍳 댓글 라우터

router.get, post, put, patch, delete 라우터 작성

🔻routes/comment.js

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
42
43
44
45
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;


🍳 라우터 연결하기

app.js에 연결

🔻app.js

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
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');

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

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('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comments', commentsRouter);

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


🍳 서버 연결하기

npm start 후 localhost:3002에 접속

콘솔에 찍히는 몽고디비 쿼리 확인

1
2
3
4
5
6
7
8
9
10
11
12
$ npm start
> learn-mongoose@0.0.1 start 
> nodemon app

[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3002 번 포트에서 대기 중
몽고디비 연결 성공
Mongoose: users.createIndex({ name: 1 }, { unique: true, background: true })

몽구스 서버 화면




💻 함께 보면 좋은 자료

몽고디비 문서

몽고디비 자료형 설명

컴퍼스 매뉴얼

몽구스 문서




📚 레퍼런스

조현영. Node.js 교과서 = Node.js Textbook / 조현영 지음 (2022). Print.

[리뉴얼] Node.js 교과서 - 기본부터 프로젝트 실습까지

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.