Mongo를 node와 연결해서 쓰기 위해 이용하는 중간 다리가 바로 Mongoose이다. 몽구스는 ODM(Object - Data - Mapper)이라고 불리는데, 여기서 object는 node, data는 mongo라고 이해하면 된다.
하나 먼저 중요한 것을 사전에 짚고 가자면 mongoose에서 쿼리 오브젝트를 설정할 땐 mongo의 db.collection.find() method 안의 query arg와 똑같이 설정하면 된다. 이것만 알면 절반은 끝난 것!
🌱 Mongoose
몽구스 프로젝트를 시작하기 위해 프로젝트 폴더에 몽구스를 다운받는다.
$ npm i mongoose
물론 그전에 npm init을 하는 것을 잊지 않았겠지?
Mongoose Docs의 링크는 위에 있다. 개발자라면 항상 공식문서를 끼고 살야아 한다는 것을 잊지 말자. 실제로 몽구스를 다루면서 겪게되는 대부분의 문제는 공식문서를 통해서 해결될 것이다. 몽구스 문법을 100% 알고 외워서 쓰는 사람은 존재.. 하겠지만 몇 명 없을것..
🌱 connect
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/myApp')
.then(() => {
console.log("CONNECTION OPEN!!!")
})
.catch(err => {
console.log("OH NO ERROR!!!!")
console.log(err)
})
DOCS를 참고하여 몽고DB와 mongoose 서버를 연결해보았다. 기본 포트넘버가 27017임을 확인할 수 있다. movieApp은 우리가 사용할 db의 이름이다.
connect는 promise를 return하므로 resolve, reject 여부에 따라 then과 catch를 사용할 수 있다. (물론 try-catch 역시 가능하다).
🌱 Schema
DB에 집어넣을 기본적인 데이터의 '틀'을 정해주는 작업이다. 스키마는 table 모양이라고 생각하면 편하다.
const <schema_name> = new mongoose.Schema({<value1>:{<option1>, ...}, <value2>:{<option1>, ... }, ... });
의 형식을 지켜서 스키마를 짜면 된다. 어떤 가게에서 파는 products들의 스키마를 만든다고 가정해보고 코드를 작성했다.
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
maxlength: 20
},
price: {
type: Number,
required: true,
min: [0, 'price must be positive number']
},
onSale: {
type: Boolean,
default: false
},
categories: [String],
qty: {
online: {
type: Number,
default: 0
},
inStore: {
type: Number,
default: 0
}
},
size: {
type: String,
enum: ['S', 'M', 'L']
}
});
원랜 name: String 이런식으로 key값과 자료형만 지정할 수도 있지만 조금 더 깐깐하게 해보았다. 어차피 나중엔 JOI라는 validating tool을 이용할 거긴 한데 위 방식과 익숙해지는 것이 좋다.
type 외에 default, required, enum, min/max, minlength/maxlength도 지정할 수 있다. [String]은 자료형이 string으로 이뤄진 array여야 한다는 뜻이고, min:[]부분은 만약 price가 0보다 작은 값을 가지면 오류 메세지로 오른쪽의 메세지가 뜨게 된다는 의미이다. qty를 보면 embedded property 역시 가능함을 볼 수 있다.
🌱 Model
우리가 짠 스키마를 컴파일 하는 과정이자 그 스키마를 가지고 생성된 모델 하나이다. 간단히 클래스라고 생각하면 편하다.
const <model_name> = mongoose.model('<model_name>',<schema_name>);
의 형식을 띈다.
const Product = mongoose.model('Product', productSchema);
model의 첫번째 argument와 변수 이름은 똑같이 설정해주는 것이 좋고,
이제 mongosh을 실행시키고 show dbs를 하면 myApp db가 생성된 것을 볼 수 있고, use myApp 입력 후 show collections를 입력하면 products라는 이름의 collection이 새로 생긴 것을 확인할 수 있다. ODM이 우리의 app과 db를 잘 연결해준 것이다.
🌱 Document
생성된 db에 실제 document 데이터를 집어넣어보자. Mongoose에서 말하는 Document란 실제 생성된 모델의 instance를 가리킨다.
const bike = new Product({
name: "Cycling Jersey",
price: 28.5,
categories: ["Cycling"],
size: "S",
});
bike는 product의 instance인 것이다. 새로운 document를 만드는 방법은 위와 같이 class instance를 만드는 것과 비슷하다. model의 parameter로서 BSON data를 전달해주는 것이다.
만들어낸 document data를 집어넣는 방법엔 두가지가 있다.
bike
.save()
.then((data) => {
console.log("IT WORKED!");
console.log(data===bike); // true
})
.catch((err) => {
console.log("OH NO ERROR!");
console.log(err);
});
1. save() method를 이용하는 법이다. save()는 promise를 return하므로 .then과 .catch를 사용할 수 있다. 만들어진 instance를 정해진 products collection에 넣어준다. .then block에서 data는 data를 콜한 bike와 같다.
Product.insertMany([bike])
.then((data) => {
console.log("saved");
})
.catch((err) => {
console.log("something wetn wrong..");
});
2. 혹은 쿼리를 이용한 방식이다. insertMany는 mongosh에서도 사용한 method인데 arg로 받은 array 안의 모델을 product collection에 집어넣는다. 역시 promise를 return하므로 try-catch, await, .then .catch 모두 사용 가능하다.
근데 실제로는 save()를 많이 사용하는듯...
🌱 Querying
mongoose에서의 쿼리는 mongosh에서 방식과 매우 흡사하다. docs를 살펴보면 어떤 조건을 가지고 object를 Find한 후 cursor 자체를 받아가거나, update하거나, delete하거나 하는 methods가 존재하는 것을 볼 수 있다.
실제 object id를 넣어줘야하는 findById~를 제외하고 method의 안에 공통적으로 들어가는 첫번째 argument는 query option이다. mongosh에서 이용한 조건문과 똑같다. findOne~은 조건에 맞는 가장 처음 발견된 object의 cursor를 return해주고.. 하는 형식이다.
어차피 각각의 문법을 완벽하게 외우고 있는 사람은 없으니 console.log와 .then을 적절히 이용해 원하는 정보를 뽑아내도록 하자.
Person.findOne({}).then((data) => {
console.log(data);
});
// or
const findFirst = async () => {
const p = await Person.findOne({});
console.log(p.first);
};
findFirst();
위 코드를 돌리면 Person에 가장 먼저 넣었던 document object가 data 결과로 나온다.
mongoose에서 위와같은 쿼리의 결과물로 return되는 것이 Query라는 오브젝트이다. Promise와 상당히 비슷한 async 객체인데 실제로 promise와 완전 똑같은 것은 아니지만 상당부분 비슷하게 작동한다고 이해해도 된다. try-catch, .then .catch, await 모두 가능하며 찾은 객체 cursor 자체로 property에게 접근하는 것 역시 가능하다. 참 편리한 객체인듯?
findOneAndDelete, findOneAndUpdate 역시 mongosh과 비슷하게 작동한다. 자세한 사항은 docs로 확인하면 되므로 여기서 굳이 추가해서 다루진 않겠다.
🌱 Virtual
실제 DB에 저장되진 않지만 application 안에서 임시로 이용할 수 있는 virtual property이다. 공식문서에 나와있는 예시를 들도록 하겠다.
const personSchema = mongoose.Schema({
first: {
type: String,
required: true,
},
last: {
type: String,
required: true,
},
});
위와 같은 스키마가 있다. 사람의 이름을 first와 last로 나눠 저장하는 스키마다. 근데 여기서 실제 DB에는 저장되진 않지만 임시로 사용할 수 있게 `${first} ${last}`로 정의되는 'fullName'이라는 property를 추가하고 싶다면?
personSchema
.virtual("fullName")
.get(function () {
//일단 getter
// instance로 접근
return `${this.last} ${this.first}`;
})
.set(function (v) {
// setter
this.first = v.substr(0, v.indexOf(" "));
this.last = v.substr(v.indexOf(" ") + 1);
});
위와같이 스키마에 .virtual을 추가하면 된다. 객체지향 프로그래밍을 공부한 적이 있다면 익숙할 getter와 setter을 만들 수 있다.
getter에는 this로 접근하여 fullName에 방금 우리가 정의한 새 string을 넣는다. fullName을 호출할 때 작동된다. setter은 fullName 속성에 무언가를 할당할 때 작동된다. indexOf(" ")를 사용해 space 기준으로 들어올 string을 잘랐다.
const p = new Person({first:"buchu",last:"kim"});
p.save();
// getter
console.log(p.fullName);
// buchu kim
// setter
p.fullName = "pa jeon";
// getter
console.log(p.fullName);
// pa jeon
getter, setter이 호출될 때마다 주석으로 표시해봤다. console.log의 결과는 아래와 같다.
🌱 MIDDLEWARE
pre/post hook이라고도 불리는 미들웨어에 관해 간단히 소개하도록 하겠다! 미들웨어는 얘 자체로 양이 방대하기 때문에 따로 게시글을 팔 것이지만 대충 어떤 개념인지 알아보면 좋을 것 같아서.
personSchema.pre("save", async function () {
console.log("ABOUT TO SAVE");
this.first = "hijacked-first";
this.last = "hijacked-last";
});
personSchema.post("save", async function () {
console.log("JUST SAVED");
});
위에 예시로 들었던 personSchema에 pre/post hook을 달아주었다. person에 save가 발동될 때마다 발동되기 직전 pre의 async function이, 발동된 직후 post의 async function이 실행될 것이다.
pre hook에서 this.first와 this.last를 hijack 했기 때문에 내가 어떤 person object를 넘기더라도 실제 DB에 저장되는 person의 이름은 모두 "hijacked-first hijacked-last"가 되어버릴 것이다..