안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[Node.js 교과서]의 책을 참고하여 포스팅한 개인 공부 내용입니다.
mongoose ODM
ODM (Object Doumment Mapping)이란?
- object와 documment를 1대 1로 짝지어 매핑
mongoose 사용하는 이유?
- 몽고디비에 없어 불편한 기능들을 보완
- 테이블과 유사한 기능, JOIN 기능을 추가
(mySQL과 유사해지기 때문에 mongoDB를 사용하는 이유(확장성과 자유로움, 가용성)를 생각하면 모순적이긴 함)
프로젝트 생성
package.json
{
"name": "learn-mongoose",
"version": "0.0.1",
"description": "learn mongoose",
"main": "app.ts",
"scripts": {
"start": "ts-node-dev --respawn --transpile-only app.ts"
},
"author": "chany",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.3.1",
"morgan": "^1.10.0",
"nunjucks": "^3.2.4"
}
}
패키지 설치
$ yarn add express morgan nunjucks mongoose
$ yarn add --dev ts-node-dev
mySQL처럼 mongodb와 node를 연결해주는 드라이버가 존재하지만, mongoose는 내장되어 있으므로 별도의 설치는 필요 없습니다.
폴더 구조
public: 공통적으로 사용되는 파일 모음 (현재는 views에서 사용될 js 파일을 담고 있음)
routes: route 파일 모음
schemas: schema 파일 모음
views: view 파일 모음
views
mongoose.html (프론트단 코드이므로 중요하지 않음)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<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>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/mongoose.js"></script>
</body>
</html>
error.html (프론트단 코드이므로 중요하지 않음)
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
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 = "";
});
schemas
user.ts
import mongoose from "mongoose";
const { Schema } = mongoose;
const userSchema = new Schema({
name: {
type: String,
require: true,
unique: true,
},
age: {
type: Number,
require: true,
},
married: {
type: Boolean,
require: true,
},
comment: String, // type만 정의할 경우 생략 가능 -> require: false
createdAt: {
type: Date,
default: Date.now,
},
});
export default mongoose.model("User", userSchema);
comment처럼 type만 지정할 경우에는 중괄호 및 type: 을 생략할 수 있습니다.
나머지 값들은 모두 기본 값으로 세팅되므로, require: false가 될 것 입니다.
mongoose.model() 메소드를 이용하여 생성한 userSchema를 가지고 User라는 모델을 생성해주었습니다.
comment.ts
import mongoose from "mongoose";
const { Schema } = mongoose;
// nested object 혹은 populator 사용
// nested object는 update, delete 시에 번거롭고, populator는 join 비용이 많이 발생(속도도 느림)
const commentSchema = new Schema({
commenter: {
type: Schema.Types.ObjectId,
require: true,
ref: "User",
},
comment: {
type: String,
require: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
export default mongoose.model("Comment", commentSchema);
commenter는 User의 id값을 가지고 있게 됩니다. mySQL로 따지자면 foreign key가 됩니다.
NoSQL에는 관계 정의가 없지만, moogose는 비슷한 역할을 할 수 있도록 지원해주며, ref를 통해 설정해줄 수 있습니다.
✅ populator 방식
→ type이 ObjectId이며, User의 _id값이 commenter의 값이 됨
→ 상대적으로 join시 비용이 많이 발생하고, 속도가 느림
→ update, delete 시 수월하다는 장점이 있음
✅ nested object 방식
→ commenter에 User 객체를 정의해줌
→ join 시 비용이 적게 들고, 속도가 빠름
→ update, delete 시 번거로움
위 방식은 populator 방식을 사용하였는데, 방식마다 장단점이 존재합니다.
index.ts
import mongoose from "mongoose";
// connect 함수 실행 시 mongoose 연결
const connect = () => {
if (process.env.NODE_ENV !== "production") {
mongoose.set("debug", true); // 콘솔에서 쿼리 확인
}
try {
// id:password@localhost~ 로그인을 위한 DB
mongoose.connect("mongodb://root:비밀번호@localhost:27017/admin", {
dbName: "nodejs", // 실제로 데이터 저장할 DB
autoIndex: true,
});
} catch (err: any) {
if (err) {
console.log("mongodb connect error", err);
} else {
console.log("mongodb connect success");
}
}
};
mongoose.connection.on("error", (err: any) => {
console.error("mongodb connect error", err);
});
mongoose.connection.on("disconnection", () => {
console.log("mongodb disconnection");
connect();
});
export default connect;
mongoDB에 mongoose를 연결해줍니다.
routes
index.ts
import express, { NextFunction, Request, Response } from "express";
import User from "../schemas/user";
const router = express.Router();
router.get("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await User.find({});
res.render("mongoose", { users });
} catch (err: any) {
console.error(err);
next(err);
}
});
export default router;
모든 User를 가져와서 mongoose.html에 값을 전달해줍니다.
users.ts
import express, { NextFunction, Request, Response } from "express";
import User from "../schemas/user";
import Comment from "../schemas/comment";
const router = express.Router();
// 모든 User 조회
router.get("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await User.find({});
res.status(200).json(users);
} catch (err: any) {
console.error(err);
next(err);
}
});
// User 생성
router.post("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await User.create({
name: req.body.name,
age: req.body.age,
married: req.body.married,
});
console.log("create user", user);
res.status(201).json(user);
} catch (err: any) {
console.error(err);
next(err);
}
});
// User Id에 댓글 가져오기
router.get("/:id/comments", async (req: Request, res: Response, next: NextFunction) => {
try {
const comments = await Comment.find({ commenter: req.params.id }).populate("commenter");
console.log("comments", comments);
res.status(200).json(comments);
} catch (err: any) {
console.error(err);
next(err);
}
});
export default router;
user의 댓글 가져오는 route를 보면, Comment.find().populate 메소드를 사용한 것을 확인할 수 있습니다.
이는, commenter에 ObjectId (_id)와 매칭되는 user의 객체를 commenter 자리에 대체 시켜줍니다.
Network Tab
commenter에 ObjectId(_id) 값이 아니라, User의 ObjectId에 대한 객체 값이 리턴된 것을 확인할 수 있습니다.
위 코드에서 populate 메소드를 제거한 후 Network Tab을 살펴보니, ObjectId 값만 리턴된 것을 확인할 수 있습니다.
comments.ts
import express, { NextFunction, Request, Response } from "express";
import Comment from "../schemas/comment";
const router = express.Router();
// 댓글 등록
router.post("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const comment = await Comment.create({
commenter: req.body.id,
comment: req.body.comment,
});
const result = await Comment.populate(comment, { path: "commenter" });
console.log("createComment Result", result);
res.status(201).json(result);
} catch (err: any) {
console.error(err);
next(err);
}
});
// 댓글 수정
router.patch("/:id", async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await Comment.updateOne(
{
_id: req.params.id,
},
{ comment: req.body.comment }
);
console.log("update result", result);
res.status(201).json(result);
} catch (err: any) {
console.error(err);
next(err);
}
});
// 댓글 삭제
router.delete("/:id", async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await Comment.deleteOne({ _id: req.params.id });
console.log("delete result", result);
res.status(201).json(result);
} catch (err: any) {
console.error(err);
next(err);
}
});
export default router;
댓글 등록 route를 살펴보면, Comment.create 후 populate을 통해 commenter에 User ObjectId에 대한 객체를 리턴할 수 있도록 처리하였습니다.
User route에서의 populate, Comment route에서의 populate 두 가지 방식이 있다는 것을 확인할 수 있습니다.
또한, mongoDB에서 생성은 save() 메소드를 사용했었는데 mongoose에서도 save 메소드로 document를 생성할 수 있습니다.
router.post("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const comment = new Comment({
commenter: req.body.id,
comment: req.body.comment,
});
await comment.save();
const result = await Comment.populate(comment, { path: "commenter" });
console.log("createComment Result", result);
res.status(201).json(result);
} catch (err: any) {
console.error(err);
next(err);
}
});
app.ts
app.ts
import express, { NextFunction, Request, Response } from "express";
import path from "path";
import morgan from "morgan";
import nunjucks from "nunjucks";
import connect from "./schemas";
import indexRouter from "./routes/index";
import usersRouter from "./routes/users";
import commentsRouter from "./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", usersRouter);
app.use("/comments", commentsRouter);
app.use((req: Request, res: Response, next: NextFunction) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
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"), "번 포트에서 대기 중");
});
'DataBase > mongoDB' 카테고리의 다른 글
mongoose) 결과값에 spread 연산자 사용 시 (0) | 2024.01.15 |
---|---|
MongoDB) Replica Set 구성 (1) | 2024.01.15 |
mongoDB) CRUD 작업하기 (0) | 2023.06.28 |
mongoDB) 데이터베이스와 컬렉션 생성하기 (0) | 2023.06.27 |
mongoDB) Mac M2 설치 중 connect ECONNREFUSED 127.0.0.1:27017 오류 (0) | 2023.06.23 |