mongoose
mongoose
node.js 환경에서 mongoDB를 다루는 라이브러리
설치, 프로젝트 세팅
npm install express
npm install mongoose
npm insatll dotenv // 환경변수를 관리해주는 라이브러리
npm install nodemon --save-dev
// package.json
{
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.19.2",
"mongoose": "^8.5.1"
},
"devDependencies": {
"nodemon": "^3.1.4"
},
"scripts": {
"dev": "nodemon app.js", // 메인 스크립트명은 관습적으로 "app"으로 사용한다
"start": "node app.js"
},
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true
},
"proxy": "http://xxx"
}
// env 파일
DATABASE_URL= mongodb+srv://<mongoDB atlas id>:<password>@cluster0.tavhtgr.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
<mongoDB atlas id>와 <password>에 각각 mongoDB atlas 아이디와 비밀번호를 넣고, 마지막 path 경로에 데이터 베이스의 이름을 삽입한다.
// app.js
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const express = require("express");
dotenv.config(); // env 파일에서 환경 변수를 process.env 객체에 할당
mongoose.connect(process.env.DATABASE_URL)
.then(() => console.log("Connected to DB")); // MongoDB와 연결
const app = express();
app.use(express.json());
이제 npm run dev 명령어를 입력하면
정상적으로 mongoDB altas와 연결되었고 프로젝트를 시작할 준비가 끝난다.
Schema
mongoDB 컬렉션에 저장될 문서의 구조를 정의하는 청사진
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema(
{
email: {
required: true,
type: String,
unique: true,
},
password: {
required: true,
type: String,
minLength: 7,
maxLength: 20
}
},
{
timestamps: true // 문서 생성시 생성, 수정 일시를 자동으로 저장하는 옵션
}
)
mongoose.Schema 생성자 함수는 문서를 정의하기 위한 객체를 인자로 받는다. 첫번째 인자에 문서의 구조를 정의하는 객체이고 선택적으로 두번째 인자에 스키마 옵션 객체를 넣을 수 있다.
스키마와 후술할 모델을 토대로 생성된 문서는 _id 필드가 있는데, 이 필드는 각 문서의 고유한 아이디이며 자동으로 생성된다. 만약 _id 필드를 생성하고 싶지 않다면 스키마를 정의할 때 _id: false 값을 넣어주면 된다.
서브 스키마
스키마 필드 값으로 다른 스키마를 사용할 수 있다. 위에서 정의한 UserSchema를 이용해보자
const mongoose = require("mongoose");
const { UserSchema } = require("./model/user.js");
const TaskSchema = new mongoose.Schema(
{
writer: UserSchema,
todo: { type: String, required: true },
completed: { type: Boolean, defalut: false }
},
{
timestamps: true
}
)
const Task = mongoose.models.Task || mongoose.model('Task', TaskSchema);
module.exports = { Task };
필드의 값으로 사용하고 싶은 스키마를 넣으면 된다. 하지만 이 방식은 스키마만을 가져오기 때문에, Task 모델로 문서를 생성할 때 wrtier 필드에 UserSchema를 만족시키는 값들을 같이 넣어줘야하는 문제가 있다. 만약 실제 존재하는 다른 모델의 문서를 참조하려면 후술할 populate()로 구현할 수 있다.
Model
mongoDB 컬렉션에 대해 CRUD 작업을 수행하기 위해 스키마를 토대로 생성한 인터페이스
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema(
{
email: {
required: true,
type: String,
unique: true,
},
password: {
required: true,
type: String,
minLength: 7,
maxLength: 20
}
},
{
timestamps: true,
}
)
const User = mongoose.models.User || mongoose.model('User', UserSchema);
mongoose.model 메서드는 문자열인 컬렉션 이름과 스키마를 인자로 받는다. 여기서 첫번째 인자는 mongoDB 컬렉션에서 변환되는데, 대문자는 소문자로 바뀌고 단수형은 복수형으로 바뀐다
이렇게 생성한 Model은 CRUD 작업을 위한 여러개의 메서드들을 제공한다. 이 메서드들 중 대다수는 Query 객체를 반환하는데, 이 쿼리 객체는 Query.prototype을 상속받아 promise처럼 체이닝하여 사용할 수 있다.
Query ?
await 키워드로 결과값만을 추출할 수 있는 thenAble 객체. (promise !== query)
User.findById(//_id값);
모델의 findById는 문서의 _id값으로 문서를 검색하는 메서드이다. 그러나 findById 메서드가 반환한 쿼리는 리스폰스의 값으로 줄 수 없다. 보통 리스폰스에 줘야하는 값은 쿼리의 결과값에 들어있다.
const user = await User.findById(//_id값);
쿼리 객체는 프로미스 객체처럼 결과값을 await 키워드로 추출할 수 있다. 그런데 우리는 간단한 검색만을 하지 않는다. 예를 들어 특정 조건을 만족하는 문서들을 최대 10개까지 생성순으로 배열에 담고 싶다고 하자. 그럼 모델의 메서드와 await으로 추출한 결과값을 다른 메서드를 사용하여 하나씩 조건을 넣어 검색해야 할까? 그렇지 않다. 쿼리 객체는 Query.prototype 을 상속받으며, 같은 쿼리 객체를 반환한다면 체이닝이 가능하다.
const userList = await User.find().sort({ createdAt: -1 }).limit(10);
// 검색 메서드는 필터를 인자로 받을 수 있는데, 필터는 각 문서의 필드와 필드 값을 가진 객체다.
// ex) Model.find({ category: "common" });
// 각 검색 메서드마다 인자의 형식이나 반환 값이 다를 수 있으니 공식문서 참조
이렇듯 쿼리는 thenAble 객체이기 때문에 CRUD 작업에 용이하다.
// 더 많은 Model 메서드: https://mongoosejs.com/docs/api/model.html
// 더 많은 Query 메서드: https://mongoosejs.com/docs/api/query.html
Document
mongoDB에 실제 저장된 문서의 필드와 필드값을 키와 값으로 가진 객체.
문서를 직접 생성해보자.
// model/user.js
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema(
{
email: {
required: true,
type: String
},
password: {
required: true,
type: String,
minLength: 7,
maxLength: 20
}
},
{
timestamps: true,
}
)
const User = mongoose.models.User || mongoose.model('User', UserSchema);
module.exports = { UserSchema, User };
// app.js
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const { User } = require("./model/user.js");
dotenv.config();
mongoose.connect(process.env.DATABASE_URL)
.then(() => console.log("Connected to DB"));
const app = express();
app.use(express.json());
app.post("/signUp", async(req, res) => {
const newUser = await User.create(req.body);
res.status(201).send(newUser);
})
app.listen(3000, () => { console.log("Server Started") });
UserSchema를 이용해 User 모델을 만든 후 유저 정보를 POST하여 문서를 생성했다. 생성된 문서는 mongoDB에서도 직접 확인할 수 있다. 또한 문서는 다른 모델의 문서를 참조할 수 있다.
// model/task.js
const mongoose = require("mongoose");
const { User } = require("./user.js");
const TaskSchema = new mongoose.Schema(
{
writer: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User", // 참조할 문서가 있는 컬렉션
},
todo: { type: String, required: true },
completed: { type: Boolean, defalut: false }
},
{
timestamps: true
}
)
const Task = mongoose.models.Task || mongoose.model('Task', TaskSchema);
module.exports = { Task };
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const { Task } = require("./model/task.js");
dotenv.config();
mongoose.connect(process.env.DATABASE_URL).then(() => console.log("Connected to DB"));
const app = express();
app.use(express.json());
app.post("/tasks", async (req, res) => {
const newTask = await Task.create({
writer: req.body.userId,
todo: req.body.todo,
completed: req.body.completed,
});
const populatedTask = await Task.findById(newTask._id).populate('writer');
// writer 필드가 참조하는 문서를 가져와서 writer에 채워준다
// create()는 Promise를 반환하기 때문에 모델, 쿼리, 문서의 메서드인 populate를 체이닝 할 수 없다
return res.status(201).send(populatedTask);
});
app.listen(3000, () => { console.log("Server Started"); });
데이터베이스에는 필드에 참조한 문서의 _id 값만 들어가지만, 리스폰스에는 참조한 문서 자체를 넣을 수 있다. 즉, 참조한 문서의 필드를 사용하려면 서버에서 처리해야 한다. populate()는 모델, 쿼리, 문서 객체의 프로토타입에 모두 정의되어 있어 체이닝을 할 수 있지만, create()는 Promise를 반환하기 때문에 체이닝할 수 없다.
// Document의 더 많은 메서드: https://mongoosejs.com/docs/api/document.html