코드코코

210926 [8장] 8.6 몽구스 사용하기 본문

기록/node.js 교과서 따라하기

210926 [8장] 8.6 몽구스 사용하기

코드코코 2021. 9. 26. 21:50

 

 

1.package.json

{
  "name": "learn-mongoose",
  "version": "1.0.0",
  "description": "몽구스를 배우자",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "codecoco",
  "license": "ISC",
  "devDependencies": {
    "nodemon": "^2.0.13"
  },
  "dependencies": {
    "express": "^4.17.1",
    "mongoose": "^6.0.7",
    "morgan": "^1.10.0",
    "nunjucks": "^3.2.3"
  }
}

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

3. views/error.html

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

4. views/mongoose.html

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>몽구스 서버</title>
    <style>
        table {
            border: 1px solid black;
            border-collapse: collapse;
        }

        table th,
        table td {
            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 type="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>
    <!-- 버튼들을 눌렀을 때, 서버의 라우터로 AJAX요청을 보내는 코드 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="/mongoose.js"></script>
</body>

</html>

4. schemas/index.js

const mongoose = require('mongoose');

//개발환경일 때만 콘솔을 통해 몽구스가 생성하는 쿼리 내용을 확인 할 수 있게 함.
const connect = () => {
    if (process.env.NODE_ENV !== 'production') {
        mongoose.set('debug', true);
    }

    //몽구스와 몽고디비를 연결
    //몽고디비 주소로 접속시도 : 
    //mongodb://root:1234@localhost:27017/admin
    mongoose.connect('mongodb://root:1234@localhost:27017/admin', {
        //옵션
        //dbName:사용할 데이터베이스
        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.log('몽고디비에 연결이 끊겼습니다. 연결을 재시도합니다.');
    connect();
});

module.exports = connect;

5.shemas/user.js

const mongoose = require('mongoose');

const { Schema } = mongoose;

//몽구스는 알아서 _id를 기본 키로 생성하므로, 별도로 필드를 적어줄 필요가 없다.
//required나 default 등의 옵션이 필요하지 않다면 간단하게 자료형만 명시해도 된다.
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);

/*
몽구스 스키마의 자료형

String
Number
Date
Buffer
Boolean
Mixed
Objectid
Array

*/

6..shemas/comment.js

const mongoose = require('mongoose');
const { Schema } = mongoose;
const { Types: { ObjectId } } = Schema;
const commentSchema = new mongoose.Schema({
    commenter: {
        //commenter 필드에 User 스키마의 사용자 ObjectId가 들어간다는 의미
        //JOIN과 비슷한 기능을 사용할 때 이용.
        type: ObjectId,
        required: true,
        ref: 'User',
    },
    comment: {
        type: String,
        required: true,
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
});

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

/*
//컬렉션 이름바꾸기
//첫글자를 소문자로 바꾼뒤 복수형으로 바꿔서 users 컬렉션을 생성함.
mongoose.model('User',userSchema,'user');
//위의 강제개명을 방지 : 세번째인수에 컬렉션명을 써준다. user_table의 컬렉션이 생성됨.
mongoose.model('User',userSchema,'user_table');
*/

7.public/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 = '';
});

8. routes/index.js

const express = require('express');

//find 사용하려면 선언과 할당.
const User = require('../schemas/user');

const router = express.Router();

router.get('/', async (req, res, next) => {
    try {
        //User.find({}) : 모든 사용자를 찾음
        //몽고디비의 db.users.find({})쿼리와 동일.
        const users = await User.find({});
        //mongoose.html을 렌더링 할 때, users 변수로 넣음.
        res.render('mongoose', { users });
    } catch (err) {
        console.error(err);
        next(err);
    }
});

module.exports = router;

9. 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.error(err);
            next(err);
        }
    })
    .post(async (req, res, next) => {
        try {
            //사용자 등록시 모델.create메서드로 저장
            //_id는 자동저장.
            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 {
        //find({commenter: req.params.id}) : 댓글을 쓴 사용자의 아이디로 조회한 뒤
        const comments = await Comment.find({ commenter: req.params.id })
            //관련있는 컬렉션의 다큐먼트를 불러옴.
            //Comment 스키마 commenter필드의 ref가 User로 되어 있으므로,
            //알아서 users컬렉션에서 사용자 다큐먼트를 찾아서 합친다.
            //commenter 필드가 사용자 다큐먼트로 치환됨.
            //commenter 필드는 OnjectId가 아니라 그 OnjectId를 가진 사용자 다큐먼트가 된다.
            .populate('commenter');
        console.log(comments);
        res.json(comments);
    } catch (err) {
        console.error(err);
        next(err);
    }
});

module.exports = router;

10. routes/comments.js

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

const router = express.Router();
//다큐먼트 등록
router.post('/', async (req, res, next) => {
    try {
        //Comment.create(): 댓글을 저장
        const comment = await Comment.create({
            commenter: req.body.id,
            comment: req.body.comment,
        });
        console.log(comment);
        //populate(): 프로미스의 결과로 반환된 comment객체에 다른 컬렉션 다큐먼트를 불러온다.
        //path 옵션으로 어떤 필드를 합칠지 설정
        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 {
     //update({어떤 다큐먼트를 수정할지를 나타낸 쿼리 객체를 제공},{수정할 필드와 값이 들어 있는 객체를 제공})
            //시퀄라이즈와는 인수의 순서가 반대.
            //몽고디비와 다르게 $set 연산자를 사용하지 않아도 기입한 필드만 바꿀 수 있음.
            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 {
            //remove({어떤 다큐먼트를 삭제할지에 대한 조건})
            const result = await Comment.remove({ _id: req.params.id });
            res.json(result);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });

module.exports = router;