1. NoSQL(Not only SQL)
- SQL을 사용하지 않는 데이터베이스이다.
- NoSQL에는 고정된 테이블이 없다.
- NoSQL에는 대표적으로 몽고디비가 있다.
1) 몽고디비의 특징
- 자유로운 데이터 입력이 가능하다.
- 컬렉션 간 JOIN을 미지원한다.
- 확장성, 가용성이 뛰어나다
2. 데이터베이스 및 컬렉션 생성하기
- nodejs라는 이름의 데이터베이스와 MySQL 테이블에 상응하는 컬렉션을 만들기
1) 데이터베이스 생성
- 데이터베이스를 만드는 명령어는 use [데이터베이스명]이다.
use nodejs
- 컬렉션은 따로 생성할 필요가 없다. 다큐먼트를 넣는 순간 컬렉션도 자동으로 생성된다.
- 직접 컬렉션을 생성하는 명령어
> db.createCollection('users')
{ "ok" : 1 }
> db.createCollection('comments')
{ "ok" : 1 }
2. CRUD 작업하기
1) Create(생성)
- 컬렉션에 컬럼을 정의하지 않아도 되므로 컬렉션에는 아무 데이터나 넣을 수 있다.
- but, 무엇이 들어올지 모른다는 단점이 있다. -> 장점이자 단점이다.
+) 몽고디비의 자료형
- 몽고디비는 자바스크립트 문법을 사용하므로 자바스크립트의 자료형을 따른다.
- Date나 정규표현식 같은 자바스크립트 객체를 자료형으로 사용할 수 있다.
- Binary Data, ObjectId, Int, Long, Decimal, Timestamp, JavaScript 등의 추가적인 자료형이 있다.
1.1 ) 실행
$ 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")
}
- 몽고디비 프롬프트를 실행해 mongosh 명령어 입력 후, nodejs 데이터베이스를 사용한다는 것을 알린다.
- db.컬렉션명.insertOne(다큐먼트)로 다큐먼트를 생성할 수 있다.
- 명령이 성공적으로 수행되었다면 acknowledged: true와 insertedId: ObjectId("5a1687007af03c3700826f70")이라는 응답이 나온다.
nodejs> db.users.find({ name: 'zero' }, { _id: 1 })
[ { "_id" : ObjectId("5a1687007af03c3700826f70") } ]
- comments 컬렉션에 데이터를 넣으면 zero의 아이디가 ObjectId("5a1687007af03c3700826f70")이라고 나온다.
2) Read(조회)
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({})는 컬렉션 내의 모든 다큐먼트를 조회하라는 뜻이다.
nodejs> db.users.find({}, { _id: 0, name: 1, married: 1 });
[
{ "name" : "zero", "married" : false },
{ "name" : "nero", "married" : true }
]
- 특정 필드만 조회하고 싶다면 find 메서드의 두 번째 인수로 조회할 필드를 넣는다.
nodejs> db.users.find({ age: { $gt: 0 }, married: true }, { _id: 0, name: 1, age: 1 });
[ { "name" : "nero", "age" : 32 } ]
- 조회 시 조건을 주려면 첫 번째 인수 객체에 기입하면 된다.
( age가 30 초과, married가 true인 다큐먼트의 이름과 나이를 조회 )
- $gt라는 특수한 속성을 사용한다.
- 몽고디비는 자바스크립트 객체를 사용해서 명령어 쿼리를 생성해야 하므로 $gt 같은 특수한 연산자가 사용된다.
- 자주 쓰이는 연산자로는 $gt(초과), $gte(이상), $lt(미만), $lte(이하), $ne(같지 않음), $or(또는), $in(배열 요소 중 하나) 등이 있다.
nodejs> db.users.find({ $or: [{ age: { $gt: 30 } }, { married: false }] }, { _id: 0, name: 1, age: 1 });
[
{ "name" : "zero", "age" : 24 },
{ "name" : "nero", "age" : 32 }
]
( age가 30 초과이거나 married가 false인 다큐먼트를 조회 )
- 몽고디비에서 OR 연산은 $or를 사용한다.
- $or에 주어진 배열 안의 조건들을 하나라도 만족하는 다큐먼트를 모두 찾는다.
*정렬
nodejs> db.users.find({}, { _id: 0, name:f, age: 1}).sort({ age: -1 })
[
{ "name" : "nero", "age" : 32 },
{ "name" : "zero", "age" : 24 }
]
- sort 메서드를 사용한다,
- -1 은 내림차순, 1은 오름차순이다.
nodejs> db.users.find({}, { _id: 0, name: 1, age: 1 }).sort({ age: -1 }).limit(1)
[ { "name" : "nero", "age" : 32 } ]
- 조회할 다큐먼트 개수를 설정할 수도 있다.
- limit 메서드를 사용한다.
nodejs> db.users.find({}, { _id: 0, name: 1, age: 1 }).sort({ age: -1 }).limit(1).skip(1)
[ { "name" : "zero", "age" : 24 } ]
- 다큐먼트 개수를 설정하면서 몇 개를 건너뛸지 설정할 수도 있다.
- skip 메서드를 사용한다.
3) Update(수정)
nodejs> db.users.updateOne({ name: 'nero' }, { $set: { comment: '안녕하세요 이 필드를 바꿔보겠습니다!' } });
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 0,
upsertedCount: 0
}
- 첫 번째 객체는 수정할 다큐먼트를 지정하는 객체이고, 두 번째 객체는 수정할 내용을 입력하는 객체이다.
- $set이라는 연산자 : 어떤 필드를 수정할지 정하는 연산자이다.
-> 연산자 사용하지 않으면 다큐먼트가 통째로 두 번째 인수로 주어진 객체로 수정되므로
일부 필드만 수정하고 싶을 때는 반드시 $set 연산자를 지정해야한다.
- 수정에 성공하면, 첫 번째 객체에 해당하는 다큐먼트 수(matchedCount)와 수정된 다큐먼트 수(modifiedCount)가 나온다.
4) Delete(삭제)
nodejs> db.users.deleteOne({ name: 'nero' })
{ acknowledged: true, deletedCount: 1 }
- 삭제할 다큐먼트에 대한 정보가 담긴 객체를 첫 번째 인수로 제공한다.
- 성공 시 삭제된 개수(deletedCount)가 반환된다.
5. 몽구스 사용하기
- MySQL에 시퀄라이즈가 있다면 몽고디비에는 몽구스(Mongoose)가 있다.
- 몽구스는 시퀄라이즈와 달리 ODM(Object Document Mapping)이라고 불린다.
- 몽고디비는 릴레이션이 아니라 다큐먼트를 사용하므로 ORM이 아니라 ODM이다.
*몽고디비 자체가 이미 자바스크립트인데도 굳이 자바스크립트 객체와 매핑하는 이유는?
- 몽고디비에 없어서 불편한 기능들을 몽구스가 보완해 주기 때문이다.
+) 보완기능
- 몽고디비는 테이블이 없어서 자유롭게 데이터를 넣을 수 있지만, 때로는 자유로움이 불편함을 초래한다.
-> 몽구스는 몽고디비에 데이터를 넣기 전에 노드 서버 단에서 데이터를 한 번 필터링하는 역할을 한다.
- MySQL에 있는 JOIN 기능을 populate라는 메서드로 어느 정도 보완한다.
-> 관계가 있는 데이터를 쉽게 가져올 수 있다.
1) 몽고디비 연결하기
- 노드와 몽고디비를 몽구스를 통해 연결하기
- 몽고디비는 주소를 사용해 연결한다.
-> 주소 형식은 mongodb://[username:password@]host[:port][/[database][?options]]와 같다.
( [ ] 부분은 있어도 되고 없어도 됨을 의미)
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,
}, (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;
- 먼저 schemas 폴더를 루트 디렉터리에 생성한다.
- ➊ 개발 환경일 때만 콘솔을 통해 몽구스가 생성하는 쿼리 내용을 확인할 수 있게 하는 코드이다.
- ➋ 몽구스와 몽고디비를 연결하는 부분이다. 몽고디비 주소로 접속을 시도하고 마한다지막 인수로 주어진 콜백 함수를 통해 연결 여부를 확인한다.
- ➌ 몽구스 커넥션에 이벤트 리스너를 달아두어 에러 발생 시 에러 내용을 기록하고, 연결 종료 시 재연결을 시도한다.
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'), '번 포트에서 대기 중');
});
- app.js를 만들고 schemas/index.js와 연결한다.
- 그 후몽고디비 서버를 먼저 실행(mongod --ipv6 --auth)하고, 콘솔을 하나 더 열어몽구스를 설치한 폴더로 이동한 후 npm start로 웹 서버를 실행한다.
3002 번 포트에서 대기 중
몽고디비 연결 성공
- 마지막 두 로그가 뜨면 연결이 성공한 것이다.
2) 스키마 정의하기
- 시퀄라이즈에서 테이블을 만들었던 것처럼 몽구스 스키마(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,
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('User', userSchema);
- schemas 폴더에 user.js와 comment.js 생성한다.
- 몽구스 모듈에서 Schema 생성자를 사용해 스키마를 만들고 필드를 각각 정의한다.
- 몽구스는 알아서 _id를 기본 키로 생성하므로 _id 필드는 적어줄 필요가 없다.
- 몽구스 스키마에서 특이한 점은 String, Number, Date, Buffer, Boolean, Mixed, ObjectId, Array를 값으로 가질 수 있다는 점이다.
-> name 필드의 자료형은 String이고 필수이며 고유한 값이어야 한다.
-> age 필드는 Number 자료형이고 필수이며, married 필드는 불 값 자료형이고 필수이다.
-> comment 필드는 String 자료형이다.
-> createdAt 필드는 Date 자료형이고 기본값은 Date.now(데이터 생성 당시의 시간)이다.
* 댓글 스키마 만들기
- 몽구스의 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과 비슷한 기능을 할 때 사용된다.
3) 쿼리 수행하기
- views 폴더 안에 mongoose.html과 error.html 파일을 만들고 , public 폴더 안에 mongoose.js 파일을 만든다.
* 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 = '';
});
* 나중에 만들 라우터들을 미리 app.js에 연결
...
const connect = require('./schemas');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const commentsRouter = require('./routes/comments');
const app = express();
...
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} 라우터가 없습니다.`);
...
* 라우터 작성
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;
- GET /로 접속했을 때 User.find({}) 메서드로 모든 사용자를 찾은 뒤, mongoose.html을 렌더링 할 때 users 변수로 넣는다.
- find 메서드는 User 스키마를 require한 뒤 사용할 수 있다.
-> 몽고디비의 db.users.find({}) 쿼리와 같다.
- 몽구스도 기본적으로 프로미스를 지원하므로 async/await과 try/catch문을 사용해서 각각 조회 성공 시와 실패 시의 정보를 얻을 수 있다.
* 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 주소로 요청이 들어올 때의 라우터이다.
- 각각 사용자를 조회하는 요청과 사용자를 등록하는 요청을 처리한다.
* 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;
- 댓글에 관련된 CRUD 작업을 하는 라우터이다.
- POST /comments 라우터는 다큐먼트를 등록하는 라우터이다.
- Comment.create 메서드로 댓글을 저장 후 populate 메서드로 프로미스의 결과로 반환된 comment 객체에 다른 컬렉션 다큐먼트를 불러온다. path 옵션으로 어떤 필드를 합칠지 설정하고 합쳐진 결과를 클라이언트로 응답한다.
- PATCH /comments/:id 라우터는 다큐먼트를 수정하는 라우터이다.
- 수정에는 update 메서드를 사용한다.
- DELETE /comments/:id 라우터는 다큐먼트를 삭제하는 라우터이다.
- remove 메서드를 사용해 삭제한다.
- npm start로 웹 서버를 실행하면 아래와 같은 화면이 나온다.
1. 빈칸 채우기 문제
(드래그하여 답을 확인해 보세요)
1. ( NoSQL )은 SQL을 사용하지 않는 데이터베이스로 대표적으로 (몽고디비)가 있다.
2. 몽고디비에서 데이터베이스를 만드는 명령어는 (use [데이터베이스명])이다.
3. 몽고디에서 생성한 다큐먼트들을 조회할 때 ( find({}) )는 컬렉션 내의 모든 다큐먼트를 조회하라는 뜻이다.
4. 몽고디비의 연산자 중에서 ($or)는 주어진 배열 안의 조건들을 하나라도 만족하는 다큐먼트를 모두 찾는다는 뜻이다.
5. MySQL에 시퀄라이즈가 있다면 몽고디비에는 ( 몽구스 )가 있다. 이는 릴레이션이 아니라 다큐먼트를 사용하므로 (ODM) 이다.
6. (몽구스)는 MySQL에 있는 JOIN 기능을( populate)라는 메서드로 어느 정도 보완해서 관계가 있는 데이터를 쉽게 가져올 수 있다.
7. CRUD 작업을 하는 라우터에서 수정에는 (update) 메서드를 사용하고 삭제에는 (remove) 메서드를 사용한다.
2. 코드 문제
1. age가 40 초과이거나 married 가 true인 목록들을 조회하는 코드를 작성해라.
nodejs> db.users.find(//여기에 코드 입력 , { _id: 0, name: 1, age: 1 });
[
{ "name" : "zero", "age" : 24 },
{ "name" : "nero", "age" : 32 }
]
답 : { $or: [{ age: { $gt: 30 } }, { married: false }] }
2. 다큐먼트를 등록하는 라우터에서 Comment.create 메서드로 댓글을 저장한 것을 적절한 메서드를 사용해서
프로미스의 결과로 반환된 해당 객체에 다른 컬렉션 다큐먼트를 불러오는 코드를 작성하고,
적절한 옵션으로 어떤 필드를 합칠지 설정 후 합쳐진 결과를 클라이언트로 응답하는 코드를 작성해라.
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_id: req.body.id,
comment: req.body.comment,
});
console.log(comment);
//여기에 코드 작성
res.status(201).json(result);
} catch (err) {
console.error(err);
next(err);
}
});
답 : const result = await Comment.populate(comment, { path: 'commenter_id' });
출처: 조현영 , 『Node.js 교과서』 개정판 3판, 길벗, 8장
[Node.js 1] 10장 웹 API 서버 만들기 (0) | 2023.12.29 |
---|---|
[Node.js 1] 9장 익스프레스로 SNS 서비스 만들기 (1) | 2023.12.22 |
[Node.js 1] 7장 MySQL (1) | 2023.11.24 |
[Node.js 1] 6장 익스프레스 웹 서버 만들기 (0) | 2023.11.17 |
[노드1] 4장~5장 http 모듈로 서버 만들기 & 패키지 매니저 (0) | 2023.11.10 |