Mongoose(with MongoDB)

백지연·2022년 1월 26일
2

NodeJS

목록 보기
14/26
post-thumbnail

이번 포스팅에서는 1. Windows에서 MongoDB 설치 및 실행, 2. MongoDB DB 생성 및 CRUD, 3. Mongoose 쿼리 수행 예(전체 코드 설명)에 대해 다루겠다.

책 Node.js 교과서(개정 2판) 책의 8장의 내용을 참고했다.
+모든 코드는 github주소에 있다.

MongoDB?

MongoDB

  • NoSQL(Not only SQL) - 관계형 데이터베이스가 아님
  • 확장성과 가용성이 좋음
  • 한 컬렉션에 다큐먼트로 다른 값의 데이터가 들어갈 수 있음

    MySQL VS MongoDB
    테이블(table) - 컬렉션(collection)
    로우(row) - 다큐먼트(document)
    컬럼(column) - 필드(field)


1. Windows에서 MongoDB 설치 및 실행

  1. 몽고디비 공식 다운 사이트에 접속한다.
  2. On-Premises 선택 후 MongoDB Community Server 탭에서 Download 를 누른다.

3. 설치 방법







4. 실행

  1. C:\에 data 폴더를 만들고 그 안에 db폴더를 만든다.
    ** 이 폴더가 없으면 실행 불가
    C:\data\db
  2. cmd를 키고 C:\Program Files\MongoDB\Server\5.0\bin 로 이동
    **5.0은 설치된 MongoDB에 따라 다를 수 있음
  3. 몽고디비 실행 명령어: mongod 입력
  • 가장 아래의 "msg"에 "Waiting for connections"가 입력되면 성공!
  • 기본적으로 27017번 포트에서 실행됨
  1. 몽고디비 프롬프트 접속:
    -1. cmd하나 더 열고 C:\Program Files\MongoDB\Server\5.0\bin 로 이동
    -2. mongo 명령어 입력
    -- 프롬프트가 >로 바뀌었다면 성공
    -- Error saving history file: FileOpenFailed: Unable to open() file No such file or directory 에러도 있는데, 그냥 history file을 찾을 수 없다는 것이므로 무시하고 넘어가겠다. 해결 방법을 아시는 분이 있으면 댓글 남겨주시면 감사합니다..
  2. 관리자 계정 추가
  • use admin 명령어
  • root 계정 생성: db.createUser({ user: '사용자 이름', pwd: '비밀번호', roles: ['root'] })
  1. mongod를 입력했었던 콘솔을 종료(ctrl+c)하고, mongod --auth 입력
    **--auth: 로그인 필요
  2. mongo를 입력했었던 콘솔을 종료(ctrl+c)하고, mongo admin -u [사용자 이름] -p [비밀번호] 입력

5. 커넥션(Connection) 생성 - feat.컴퍼스

** windows는 MongoDB를 설치하면서 컴퍼스가 자동으로 설치된다.

  1. 4(실행)-6, 4(실행)-7이 되어있는 상태에서 MongoDBCompass 프로그램 실행

  1. New Connection 화면에서 Fill in connection fields individually 클릭

  1. -1. Authentication : Username/Password로 변경
    -2. MongoDB때 생성한 Username과 Password 입력
    -3. Connect 클릭

  1. localhost:27017에 접속됨, 기본적으로 admin, config, local 데이터베이스가 존재

2. MongoDB DB 생성 및 CRUD 명령어

1. DB 생성

  • 데이터베이스 생성, 해당 db 사용 알림: use [데이터베이스명]

  • 데이터베이스 목록 확인: show dbs
    **use로 생성한 nodejs가 안 보이지만 최소 한 개 이상의 데이터가 있어야 보임

  • 현재 사용중인 데이터베이스 확인: db

  • 컬렉션 생성: db.createCollection('[컬렉션명'])

  • 컬렉션 목록 확인: show collections

2. CRUD

C: CREATE(생성)

  • 데이터 입력: db.컬렉션명.save(다큐먼트)

데이터 생성 EXAMPLE )

db.users.save({ name: 'zero', age: 24, married: false, comment: '안녕하세요. 간단히 몽고디비 사용 방법에  대해 알아봅시다.' , createAt: new Date() });

출력(console)

R: READ(조회)

  • 조건이 있는 데이터 조회: db.users.find({});

users 조회 EXAMPLE )

  • 데이터 조회: db.[다큐먼트].find({조건}, {조회할 필드})

특정 조건으로 조회 EXAMPLE )

특정 조건에 속성 사용 EXAMPLE )

자주 쓰이는 연산자
$gt(초과), $gte(이상), $lt(미만), $lte(이하), $ne(같지 않음), $or(또는), $in(배열 요소 중 하나)

EXAMPLE )
입력

db.users.find({ age: {$gt: 30}, married: true}, {_id:0, name: 1, age: 1});

출력

{ "name" : "nero", "age" : 32 }

특정 필드만 조회 EXAMPLE )

정렬, 개수 제한, 건너뛸 개수지정 조회 EXAMPLE )
**정렬: .sort([조건]), 개수 제한: .limit([개수]), 건너뛸 개수: .skip([개수])

U: UPDATE(수정)

  • 데이터 수정: db.컬렉션명.update({다큐먼트}, {수정할 내용})

UPDATE EXAMPLE )

 db.users.update({name:'nero'}, {$set: { comment: '안녕하세요. 검은 고양이 nero 아닙니다.' }});

출력 **첫 번째 객체에 해당하는 다큐먼트 수(nMatched)와 수정된 다큐먼트 수(nModified)가 반환

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

D: DELETE(삭제)

  • 데이터 삭제: db.컬렉션명.update({다큐먼트})

UPDATE EXAMPLE )

db.users.remove({name:'nero'})

출력 **성공 시 삭제된 개수가 반환

WriteResult({ "nRemoved" : 1 })

3. Mongoose 쿼리 수행 예(전체 코드 설명)

Mongoose는 Sequelize와 다르게 ODM(Object Document Mapping)이다. 릴레이션이 아니라 다큐먼트를 사용하기 때문이다.

이전에 포스팅했던 Sequelize(with MySQL) -2의 전체 코드와 동일한 작업을 하는 코드를 작성해보겠다. 해당 포스팅에 설명이 꼼꼼하고, 코드도 매우 유사하므로 이해가 안 되면 찾아가보자!

Mongoose 환경 세팅

npm 설치 후 아래의 모듈 설치

+ [설치할 모듈]

Git [learn-mongoose/package.json]

{
  "name": "delay100_mongoose",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "delay100",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.2",
    "mongoose": "^6.1.8",
    "morgan": "^1.10.0",
    "nunjucks": "^3.2.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

Git [learn-mongoose/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.envPORT || 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'), '번 포트에서 대기 중');
});

Git [learn-mongoose/views/mongoose.html]

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>몽구스 서버</title>
        <style>
            table {
                border: 1px solid black;
                border-collapse: collapse;
            }
            table td,
            table th {
                border: 1px solid black;
            }
        </style>
    </head>
    <body>
        <div>
            <form id="user-form">
                <fieldset>
                    <legend>사용자 등록</legend>
                    <div><input id="username" type="text" placeholder="이름"></div>
                    <div><input id="age" type="number" placeholder="나이"></div>
                    <div><input id="married" type="checkbox">
                        <label for="married">결혼여부</label>
                    </div>
                    <button type="submit">등록</button>
                </fieldset>
            </form>
        </div>
        <br>
        <table id="user-list">
            <thead>
                <tr>
                    <th>아이디</th>
                    <th>이름</th>
                    <th>나이</th>
                    <th>결혼여부</th>
                </tr>
            </thead>
            <tbody>
                {% for user in users %}
                    <tr>
                        <td>{{user.id}}</td>
                        <td>{{user.name}}</td>
                        <td>{{user.age}}</td>
                        <td>{{'기혼' if user.married else '미혼'}}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
        <br>
        <div>
            <form id="comment-form">
                <fieldset>
                    <legend>댓글 등록</legend>
                    <div><input id="userid" type="text" placeholder="사용자 아이디"></div>
                    <div><input id="comment" type="text" placeholder="댓글"></div>
                    <button input="input" id="submit">등록</button>
                </fieldset>
            </form>
        </div>
        <br>
        <table id="comment-list">
            <thead>
                <tr>
                    <th>아이디</th>
                    <th>작성자</th>
                    <th>댓글</th>
                    <th>수정</th>
                    <th>삭제</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="/mongoose.js"></script>
    </body>
</html>

Git [learn-mongoose/views/error.html]

<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>

Git [learn-mongoose/schemas/index.js]

// sequelize의 테이블 만드는 것
const mongoose = require('mongoose');

const connect = () => {

    // 배포 환경이 아닌 경우(ex. 개발 환경) debug를 세팅함 - 몽구스가 생성하는 쿼리를 콘솔에 출력
    if (process.env.NODE_ENV !== 'production') {
        mongoose.set('debug', true);
    }

    // 몽구스와 몽고디비 연결
    mongoose.connect('mongodb://jiyeon:system@127.0.0.1:27017/admin', { // mongodb://이름:비밀번호@host:27017/admin
        dbName: 'nodejs', // 접속을 시도하는 주소의 데이터베이스
        useNewUrlParser: true, // 굳이 없어도 되는데 콘솔에 에러 뜨는 것 없애기1 
        useCreateIndex: true, // 굳이 없어도 되는데 콘솔에 에러 뜨는 것 없애기2
    }, (error) => {
        if (error){
            console.log('몽고디비 연결 에러', error);
        } else {
            console.log('몽고디비 연결 성공');
        }
    });
};
// 이벤트 리스너
mongoose.connection.on('error', (error) => {
    console.log('몽고디비 연결 에러', error); // 에러 발생 시 에러 내용을 기록하고,
});

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

module.exports = connect;

Git [learn-mongoose/schemas/user.js]

// sequelize의 models와 동일
const mongoose = require('mongoose');

const {Schema} = mongoose;
const userSchema = new Schema({ // 스키마를 생성
    // _id를 기본 키로 생성하므로 _id 필드는 적어줄 필요가 없음
    name: {
        type: String, // 타입(자료형)을 String으로 지정
        require: true, // 필수
        unique: true, // 고유 값
    },
    age: {
        type: Number, // 타입을 Number로 지정
        require: true, 
    },
    married: {
        type: Boolean,
        required: true,
    },
    comment: String,
    createdAt: {
        type: Date,
        default: Date.now, // 기본 값: 데이터 생성 당시의 시간
    },
});

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

Git [learn-mongoose/public/mongoose.js]

const mongoose = require('mongoose');

const { Schema } = mongoose; 
const { Types: { ObjectId} } = Schema;
const commentSchema = new Schema({
    commenter: { 
        type: ObjectId, 
        require: true,
        ref: 'User', // 스키마의 사용자 ObjectId가 들어감 - 몽구스가 JOIN과 비슷한 기능을 할 때 사용됨
    },
    comment: {
        type: String,
        required: true,
    },
    createdAt: {
        type: Date,
        default: Date.now, // 데이터 생성 당시 시간
    },
});

module.exports = mongoose.model('Comment', commentSchema);
// 사용자 이름을 클릭할 때 댓글 로딩
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);
            });
            // row 셀 추가
            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.log(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) {
            // row 셀 추가
            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.log(err);
                }
            });
            const remove = document.createElement('button');
            remove.textContent = '삭제';
            remove.addEventListener('click', async() => { // 삭제 클릭 시
                try {
                    await axios.delete(`/comments/${comment._id}`);
                    getComment(id);
                } catch(err){
                    console.log(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.log(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.log(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.log(err);
    }
    e.target.userid.value='';
    e.target.comment.value='';
});

Git [learn-mongoose/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.log(err);
        next(err);
    }
});

module.exports = router;

Git [learn-mongoose/routes/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.log(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.log(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.log(err);
        next(err);
    }
});

module.exports = router;

Git [learn-mongoose/routes/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.log(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.log(err);
            next(err);
        }
    })
    .delete(async (req, res, next)=>{
        try {
            const result = await Comment.remove({ _id: req.params.id});
            res.json(result);
        } catch(err){
            console.log(err);
            next(err);
        }
    });

module.exports = router;

--

실행 화면

** 실행 시 서버를 꼭 켜줘야 함
입력(cmd)

C:\Program Files\MongoDB\Server\5.0\bin> mongod --auth

실행화면(console)

실행화면(웹 브라우저) - http://127.0.0.1:3002

+세부적인 실행 화면은 이전 포스팅 참고


공부중에 mongoose.js의 가장 위에있는 함수인 document.querySelectorAll('#user-list tr').forEach((el)=> {getUser()함수의 row.addEventListener('click',을 왜 두개나 쓰는지 이해가 안 됐다. 둘 다 row를 클릭했을 때 실행되는 것이 아닌가? 그럼 중복 코드가 아닌가? 생각했다.

=> 알게된 점
document.querySelectorAll('#user-list tr').forEach((el)=> { 함수는 html이 처음 로딩됐을 때 실행되고(이미 있는 데이터), row.addEventListener('click', 함수는 새로운 데이터를 추가한 경우 row 클릭 시 댓글을 로딩하기 위함이었다.

sequelize를 공부할 때는 알지 못했는데, 한 번 더 코드를 보면서 이해한걸 보니 역시 코드는 여러번 보는 게 중요한가 보다,,
그래도 sequelize와 mongoose가 비슷한 부분이 많아서 주석/설명없이 혼자 이해할 수 있는 부분이 많았다!

profile
TISTORY로 이사중! https://delay100.tistory.com

2개의 댓글

comment-user-thumbnail
2022년 1월 26일

블로그가 알차네요~ 스크린캡처 야무지시네용

1개의 답글