본문 바로가기

Node.js

Node.js - 몽구스 (Mongoose, 몽고디비 작업 라이브러리)

 

몽구스는 노드와 몽고디비를 연동해줄 뿐만 아니라 쿼리까지 만들어준다. 몽고디비 자체도 자바스크립트 쿼리를 사용하긴 하지만 이 라이브러리를 이용하면 더 쉽게 만들 수 있다.

 

귀여워

 

몽구스는 시퀄라이즈와는 달리 ODM (Object Document Mapping) 이라고 불린다. 몽고디비는 릴레이션이 아닌 도큐멘트를 사용하므로 ORM이 아니라 ODM이다. 몽고디비 자체가 이미 자바스크립트인데 왜 굳이 자바스크립트 객체와 매핑하는 것일까? 이는 몽고디비에 없어서 불편한 점들을 몽구스가 보완해주기 때문이다.

 

먼저 스키마라는 것이 생겼다. 몽고디비는 테이블이 없어 자유롭게 데이터를 넣을 수 있지만, 오히려 이게 독이될 수도 있다. 실수로 잘못된 자료형의 데이터를 넣거나 다른 도큐멘트에는 없는 필드의 데이터를 넣을 수도 있다. 몽구스는 몽고디비에 데이터를 넣기 전 노드 서버 단에서 데이터를 한 번 필터링하는 역할을 해준다.

 

또한 MySQL의 JOIN 기능을 populate 메서드로 어느 정도 보완해줄 수 있다. 따라서 관계가 있는 데이터를 쉽게 가져올 수 있다. 비록 쿼리 한 번에 데이터를 합쳐서 가져오지는 않지만, 이 작업을 내가 직접 해줄 필요는 없으므로 편리하다. 또한, 프로미스 문법과 강력하고 가독성이 높은 쿼리 빌더를 지원하는 것도 장점이다. 몽구스를 위한 새 프로젝트를 생성해보자. learn-mongoose 폴더를 만들어 npm init으로 아래와 같이 package.json을 만들어준다.

 

{
  "name": "learn-mongoose",
  "version": "0.0.1",
  "description": "Let's learn mongoose",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app"
  },
  "author": "Effort",
  "license": "MIT"
}

 

그 후 express, morgan, nunjucks, mongoose를 설치하고 nodemon도 개발 모드로 설치해주자.

 

몽고디비 연결

노드와 몽고디비를 연결해보자. 몽고디비는 주소를 사용하여 연결하는데, 주소 형식은 mongodb://[유저이름:비밀번호@]호스트 주소[:포트][/데이터베이스][?옵션] 이다. 대괄호로 묶인 부분들은 있어도 되고 없어도 된다.

 

먼저 schemas 폴더를 루트 디렉터리에 생성하고, 폴더 안에 index.js 파일을 생성한 후 다음과 같이 작성한다.

 

const mongoose = require('mongoose');

const dbUrl = 'mongodb://' +
    'Beom%20Seok:.......!!@' +
    'localhost' +
    ':27017' +
    '/admin';

const connect = () => {
    if (process.env.NODE_ENV !== 'production') {
        mongoose.set('debug', true);
    }
    mongoose.connect(dbUrl, {
        dbName: 'nodejs',
        useNewUrlParser: true,
        useCreateIndex: true,
    }, (err) => {
        if (err) {
            console.error('몽고디비 연결 에러', err);
        } else {
            console.log('몽고디비 연결 성공');
        }
    });
};
mongoose.connection.on('error', (err) => {
    console.error('몽고디비 연결 에러', err);
});
mongoose.connection.on('disconnected', () => {
    console.error('몽고디비 연결이 끊겼습니다. 연결을 재시도합니다.');
    connect();
});

module.exports = connect;

 

dbUrl에 주소를 적어두었다. 내 유저이름이 "Beom Seok" 인데, 주소에는 띄어쓰기가 들어가면 안되기 때문에 %20으로 대체해주었다 (근데 확인해보니 그냥 띄어쓰기를 넣어도 알아서 작동한다). connect 함수를 export 하며, 개발환경일 경우 mongoose.set('debug', true) 를 통해 생성하는 쿼리 내용을 확인할 수 있게 해준다.

 

mongoose.connect 부분은 몽고디비 주소로 접속을 시도한다. 시도하는 주소의 데이터베이스는 admin이지만, 실제 사용할 데이터베이스는 nodejs이므로 두 번째 인수로 dbName 옵션을 주어 nodejs 데이터베이스를 사용한다. 마지막 인수로 주어진 콜백 함수를 통해 연결 여부를 확인한다.

 

mongoose.connection.on은 커넥션에 이벤트 리스너를 달게 해준다. 이러 발생 시 에러 내용을 기록하고, 연결 종료 시 재연결을 시도한다.

 

이제 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 || 3000);
app.set('view engine', 'html');
nunjucks.configure('views', {
    express: app,
    watch: true,
});
connect();

// 미들웨어
app.use(morgan('def'));
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 로 실행해보면

 

3000 번 포트에서 대기 중
몽고디비 연결 성공

 

위와 같이 잘 실행되는 것을 볼 수 있다.

 

스키마 정의

이제 몽구스 스키마를 만들어보자. 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);

 

Schema 생성자로 스키마를 만들어준다. 시퀄라이즈에서 모델을 정의하는 것과 비슷하며, 필드를 각각 정의해주면 된다. 몽구스는 알아서 _id를 기본 키로 생성하므로 적어줄 필요가 없다. 나머지 필드의 스펙만 입력해주자.

 

몽구스 스키마에서 특이한 점은 String, Number, Date, Buffer, Boolean, Mixed, ObjectId, Array를 값으로 가질 수 있다는 점이며, 몽고디비의 자료형과 조금 다르고 편의를 위해 종류 수를 줄였다.

 

위의 예제에서 name 필드의 자료형은 String이며 값은 고유해야 한다. required나 default 등의 옵션이 필요하지 않다면 간단히 자료형만 명시하면 된다.

 

마지막 exports 는 몽구스의 model 메서드로 스키마와 몽고디비 컬렉션을 연결하는 모델을 만든다.

 

이제 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
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
});

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

 

commenter 속성의 자료형이 ObjectId이다. 옵션으로 ref 속성에 User을 넣어주었다. 이는 commenter 필드에 User 스키마의 사용자 ObjectId가 들어간다는 뜻이다. 이는 몽구스가 JOIN과 비슷한 기능을 할 때 사용된다.

 

더보기

몽구스는 모델 메서드의 첫 번째 인수로 컬렉션 이름을 만든다. 첫 번째 인수가 User라면, 첫 글자를 소문자로 만든 뒤 복수형으로 바꾼 users 라는 컬렉션을 생성한다. 즉 Comment라면 comments 컬렉션이 생성될 것이다. 하지만 커스텀 컬렉션 이름을 사용하고 싶다면, 세 번째 인수로 컬렉션 이름을 주면 된다.

즉, mongoose.model('User', userSchema, 'user_table'); 과 같은 방식으로 작성할 수 있다.

 

쿼리 수행

이제 쿼리를 수행해보자. views 폴더 안에 mongoose.html과 error.html 파일을 만든다. 프론트 엔드 코드는 저번처럼 레포지토리에서 가져오자. mongoose.html, error.html, 그리고 mongoose.js (퍼블릭) 을 가져온다.

 

조금 뒤에 만들 라우터들을 미리 app.js에 연결시켜 주자.

 

...

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

...

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

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

...

 

인덱스 라우터를 다음과 같이 작성해준다.

 

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;

 

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

 

GET /users 와 POST /users 주소로 요청이 들어올 때의 라우터이다. GET / 과는 달리 GET /users 에서는 데이터를 JSON 형식으로 반환한다.

 

사용자를 등록할 때는 먼저 모델.create 메서드로 저장한다. 정의한 스키마에 부합하지 않는 데이터는 몽구스가 에러를 발생시킨다. _id는 자동 생성된다.

 

GET /users/:id/comments 라우터는 댓글 도큐멘트를 조회하는 라우터이다. find 메서드에는 옵션이 추가돼 있으며, 댓글 조회 후의 populate 메서드는 관련 있는 컬렉션의 도큐멘트를 불러온다. 이 때, Comment 스키마에서 commenter 필드의 ref가 User로 되어있기 때문에, 자동으로 users 컬렉션에서 사용자 도큐멘트를 찾아 합친다. 이후 commenter 필드가 사용자 도큐멘트로 치환된다. 이제 commenter 필드는 ObjectId가 아닌 그 ObjectId를 가진 사용자 도큐멘트가 된다.

 

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.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 로 도큐멘트 삭제가 가능하게 했다. POST /comments 의 경우 댓글을 Comment.create 메서드로 저장한 후, populate 메서드로 프로미스의 결과로 반환된 comment 객체에 다른 컬렉션 도큐멘트를 불러와 저장한다. path 옵션으로 어떤 필드를 합칠지 설정하면 된다.

 

PATCH /comments/:id 라우터는ㄴ 도큐멘트를 수정하게 해준다. 수정에는 update 메서드를 사용하며, 첫 번째 인수는 어떤 도큐멘트를 수정할지를 나타낸 쿼리 객체 (이 경우 id 지정) 를 담으며, 두 번째 인수는 수정할 필드와 값이 들어 있는 객체를 담는다. 몽고디비와는 다르게, mongoose에서는 $set 연산자를 사용하지 않아도 기입한 필드만 바꾼다. 따라서 실수로 도큐멘트를 통째로 수정할 일이 없어 안전하다. DELETE /comments/:id 라우터는 도큐멘트를 삭제하는 라우터로, remove 메서드를 사용하여 삭제한다. 어떤 도큐멘트를 삭제할지에 대한 조건을 첫 번째 인수로 넣어준다.

 

이제 실행을 한 후, localhost에 3000번 포트에서 접속해보자.

 

위와 같이 잘 실행되는 것을 볼 수 있다.

 

 

출처

Node.js 교과서 개정 2판 - 길벗, 조현영