본문 바로가기

BackEnd/Node

[노드교과서] 섹션 7. MongoDB

8.1 NoSQL vs SQL

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

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

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

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

 

8.2 데이터베이스와 컬렉션 만들기

 1. 데이터베이스 생성하기

admin> use nodejs;
switched to db nodejs
nodejs> show dbs;
admin   132.00 KiB
config   60.00 KiB
local    72.00 KiB

 2. 컬렉션 생성하기

nodejs> db.createCollection('users')
{ ok: 1 }
nodejs> db.createCollection('comments')
{ ok: 1 }
nodejs> show dbs
admin   132.00 KiB
config   60.00 KiB
local    72.00 KiB
nodejs   16.00 KiB

8.3 CRUD 작업하기

 1. create

  ⇒ 몽고디비는 컬럼을 정의하지 않아도 된다.

   - 자유로움이 장점이지만 무엇이 들어올지 모른다는 단점도 존재한다.

   - 자바스크립트의 자료형을 따른다(차이점도 존재)

   - ObjectId: 몽고디비의 자료형으로 고유 아이디(PK) 역할을 한다.

   - insertOne method로 저장한다.

 

nodejs> db.users.insertOne({name:'zero', age:24, married:false, comment:'안녕하세요', createAt : new Date()});
{
  acknowledged: true,
  insertedId: ObjectId('659a9933dd7b2316d1903de4')
}
nodejs> db.users.insertOne({name:'jaeik', age:39, married:true, comment:'안녕하세요. 김재익입니다.', createAt : new Date()});
{
  acknowledged: true,
  insertedId: ObjectId('659a9966dd7b2316d1903de5')
}

 2. Create(관계 설정)

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

   - 사용자의 ObjectId를 찾아 댓글 컬렉션에 넣는다.

nodejs> db.comments.insertOne({ commenter : ObjectId('659a9933dd7b2316d1903de4'), comment: '댓글을 달아봅시다'});
{
  acknowledged: true,
  insertedId: ObjectId('659a99dcdd7b2316d1903de6')
}

 3. Read

  ⇒ find로 조회, findOne으로 하나만 조회힌다.

nodejs> db.users.find({});
[
  {
    _id: ObjectId('659a9933dd7b2316d1903de4'),
    name: 'zero',
    age: 24,
    married: false,
    comment: '안녕하세요',
    createAt: ISODate('2024-01-07T12:29:39.641Z')
  },
  {
    _id: ObjectId('659a9966dd7b2316d1903de5'),
    name: 'jaeik',
    age: 39,
    married: true,
    comment: '안녕하세요. 김재익입니다.',
    createAt: ISODate('2024-01-07T12:30:30.025Z')
  }
]
nodejs> db.comments.find({});
[
  {
    _id: ObjectId('659a99dcdd7b2316d1903de6'),
    commenter: ObjectId('659a9933dd7b2316d1903de4'),
    comment: '댓글을 달아봅시다'
  }
]

 4. Read(조건)

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

  nodejs> db.users.find({},{ _id:0, name:1, married:1});
  [ { name: 'zero', married: false }, { name: 'jaeik', married: true } ]

 

  ⇒ 첫 번째 인수로 조회조건 입력이 가능하다.

   - $gt, $or 같은 조건 연산자를 사용할 수 있다.

nodejs> db.users.find({ age : { $gt : 30 }, married : true},{ _id:0, name:1, married:1});
[ { name: 'jaeik', married: true } ]
nodejs> db.users.find({ $or : [{age : { $gt : 30 }}, {married : false}]},{ _id:0, name:1, married:1});
[ { name: 'zero', married: false }, { name: 'jaeik', married: true } ]

 5. Read(조건)

  ⇒ 정렬은 sort 메서드로 한다.(1: 오름차순, -1: 내림차순)

  nodejs> db.users.find({},{ _id:0, name:1, age:1}).sort({age : -1});
  [{ name: 'jaeik', age: 39 }, { name: 'zero', age: 24 } ]

 

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

  nodejs> db.users.find({},{ _id:0, name:1, age:1}).sort({age : -1}).limit(1)
[ { name: 'jaeik', age: 39 } ]

 

  ⇒ skip 메서드로 건너뛸 다큐먼트 개수를 설정한다.

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

 6. update

  ⇒ updateOne, updateMany 메서드로 쿼리를 수행한다.

   - 첫 번째 인수로 수정대상을, 두 번째 인수로 수정 내용을 제공한다.

   - 두 번재 인수의 시작으로 $set을 붙이지 않으면 다큐먼트 전체가 대체되므로 주의해야 한다.

   - 결과로 찾은 개수, 수정된 개수 등의 정보가 표시된다.

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

 7. delete

  ⇒ deleteOne, deleteMany 메서드로 쿼리를 수행한다.

   - 첫 번재 인수로 삭제할 대상 조건을 제공한다.

   - 성공 시 삭제된 개수가 반환된다.

nodejs> db.users.deleteOne({name:'zero'});
{ acknowledged: true, deletedCount: 1 }
nodejs> db.users.find({});
[
  {
    _id: ObjectId('659a9966dd7b2316d1903de5'),
    name: 'jaeik',
    age: 39,
    married: true,
    comment: '안녕하세요. 이 필드를 바꾸겠습니다.',
    createAt: ISODate('2024-01-07T12:30:30.025Z')
  }
]

8.5 몽구스 사용하기

 1. 몽구스 ODM

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

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

   - 몽구스는 mySQL과 다르게 몽고디비 드라이버를 안 받아도 된다.(몽구스 안에 드라이버가 내장)

   

 2. 몽고디비 연결하기

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

   - 인증은 admin 데이터베이스에서, 서비스는 dbName 데이터베이스에서 한다.

const mongoose = require('mongoose');

const connect = () => {
    if(process.env.NODE_ENV !== 'production') {
        mongoose.set('debug', true);
    }

    mongoose.connect('mongodb://root:0000@localhost:27017/admin', {
        dbName : 'nodejs',
    }).then(() =>{
        console.log('몽고디비 연결 성공');
    }).catch((err) => {
        console.error('몽고디비 연결 에러', err);
    });

    mongoose.connection.on('error', (error) => {
        console.error('몽고디비 연결 에러', error);
    });

    mongoose.connection.on('disconnected', () => {
        console.error('몽고디비 연결이 끊겼습니다.  연결을 재시도합니다.');
        connect();
    });
};

module.exports = connect;

 3. 앱과 연결하기

  ⇒ app.js로 연결

   - schemas/index.js의 함수가 실행된다.

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

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

   - 연결이 끊기면(disconnection) 다시 연결을 시도한다.

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

 4. 스키마 작성하기

  ⇒ schemas 폴더 안에 생성한다.

   - MySQL의 테이블처럼 정해진 데이터만 들어갈 수 있게 강제한다.

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

// schema/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,
    createAt : {
        type : Date,
        default : Date.now,
    },
});

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

 

// schemas/comment.js

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,
    },
    createAt : {
        type : Date,
        default : Date.now,
    }
});
module.exports = mongoose.model('Comment', commentSchema);

 

 5. 라우터 작성하기

  ⇒ 프론트앤드 코드는 서버에 요청을 보내는 AJAX 요청 위주로만 보는 것이 좋다.
  ⇒ 서버 코드는 응답을 보내는 라우터 위주로 살펴보는게 좋다.

 6. 사용자 라우터 작성하기

  ⇒ router.get, post, put, patch, delete 라우터를 작성한다.

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 {
            // populate는 RDB의 JOIN의 역할을 수행한다.
            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;

 7. 댓글 라우터

  ⇒ router.get, post, put, patch, delete 라우터를 작성한다.

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,
            commnet : 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.commnet,
            },);
            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;

 8. 라우터 연결하기

  ⇒ 기존 작성해 둔 app.js에 라우터를 연결한다.

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

const connect = require('./schemas');
const indexRouter = require('./routes');
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);
});
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'), '번 포트에서 대기중');
});