mongoose로 CRUD operation 구현하기 - edit video / delete video / search video
Model.exists() & Model.findByIdAndUpdate() / pre middleware / statics / 데이터 정렬 / req.query / mongoDB 정규 표현식
hashtags input의 value 값으로 video.hashtags.join()
을 입력해 화면에는 배열이 아니라 쉼표(,)로 구분된 문자열
로 표시되도록 한다.
//- edit.pug
extends base
block content
h4 Update Video
form(method="POST")
input(name="title", placeholder="Title", value =video.title, required, maxlength=80)
input(name="description", placeholder="Description", value=video.description, required, minlength=20)
input(name="hashtags", placeholder="Hashtags, separated by comma", value=video.hashtags.join(), required)
input(value="edit", type="submit")
getUpload 컨트롤러와 마찬가지로 입력 form이 있는 edit 페이지
를 렌더링한다.
차이점은 getEdit 컨트롤러는 데이터베이스로부터 video
를 찾아 함께 불러온다는 것이다.
// videoController.js
export const getEdit = async (req, res) => {
const { id } = req.params;
const video = await Video.findById(id);
if (!video) {
return res.render("404", { pageTitle: "Video not found" });
}
return res.render("edit", { pageTitle: `Edit ${video.title}`, video });
};
postUpload 컨트롤러에서 사용자가 form에 입력한 각 hashtag 앞에 #을 추가하여 배열의 형태로 데이터베이스에 저장하도록 코드를 작성했었다.
즉, 수정을 위해 데이터베이스로부터 가져온 각 hashtag 문자열에는 #이 포함되어 있을 것이다.
이때 수정을 하면 각 hashtag 앞에는 #이 하나 더 추가된 채로 데이터베이스에 저장된다.
다시 수정을 위해 hashtags를 불러오면 #이 2개가 되어 있다.
이 문제를 해결하기 위해서는 각 hashtag가 #으로 시작하지 않을 때만, 데이터베이스에 저장 시 #을 추가하도록
코드를 수정해야 한다.
postUpload 컨트롤러와 postEdit 컨트롤러를 모두 수정하도록 한다.
// videoController.js
export const postEdit = async (req, res) => {
const { id } = req.params;
const { title, description, hashtags } = req.body;
// find
const video = await Video.findById(id);
if (!video) {
return res.render("404", { pageTitle: "Video not found" });
}
// update
video.title = title;
video.description = description;
video.hashtags = hashtags
.split(",")
.map((word) => (word.startsWith("#") ? word : `#${word}`));
// save
await video.save();
return res.redirect(`/videos/${id}`);
};
Model.exists()
Model.exists()는 인수로 filter를 받는다.
{ } 안에 데이터 oject의 어떤 property든지 검색 가능하다.
boolean 값을 return 한다.
이제 video 변수는 video object를 받는 대신에 true/false 값을 받는다.
이는 video object를 그대로 가져오는 것보다 효율적이다.
한편, getEdit 컨트롤러에서는 Video.exists()를 사용할 수 없다.
getEdit 컨트롤러는 에러 방지를 위해 video의 존재 여부만 확인하는 게 아니라 아예 video를 화면에 가져오기 위해 video object를 필요로 하기 때문이다.
Model.findByIdAndUpdate()
Model.findByIdAndUpdate()는 첫 번째 인수로 _id 값을, 두 번째 인수로 변경할 내용을 받는다.
find와 update, save 과정을 한 번에 줄여서 할 수 있다.
// videoController.js
export const postEdit = async (req, res) => {
const { id } = req.params;
const { title, description, hashtags } = req.body;
const video = await Video.exists({ _id: id });
if (!video) {
return res.render("404", { pageTitle: "Video not found" });
}
await Video.findByIdAndUpdate(id, {
title,
description,
hashtags: hashtags
.split(",")
.map((word) => (word.startsWith("#") ? word : `#${word}`)),
});
return res.redirect(`/videos/${id}`);
};
💡 mongoose middleware
mongoose middleware는 어떤 이벤트가 발생하기 전에 document를 가로채서 수정
하여 그 형식 및 내용대로 저장되도록 한다.
예를 들어, user를 생성하기 전에 eamil이 실제로 존재하는 email인지 확인하거나 comment를 저장하기 전에 비속어가 포함되어 있는지 확인해야 한다.
그러나, 이런 코드들은 유효성 검사와 비슷한 역할을 하는 코드로, 컨트롤러마다 복붙하며 사용하는 것은 비효율적이다.
mongoose는 이를 schema 단에서 처리해주는 기능을 제공한다.
middleware의 종류로는 pre, post hook 등이 있다.
여기서는 video를 생성 혹은 업데이트 하기 전에 그 프로세스를 잠시 중단하고, pre middleware를 이용해 해시태그를 깔끔하게 정리한 후, 다시 프로세스를 시작하려고 한다.
// videoController.js
await Video.findByIdAndUpdate(id, {
title,
description,
hashtags, // 수정
});
// Video.js
const videoSchema = mongoose.Schema({
// 생략
});
videoSchema.pre("save", async function() {
console.log(this); // this를 출력해봄
});
const Video = mongoose.model("Video", videoSchema);
그 후 Video.js 파일에 pre middelware를 생성한다.
※ middleware는 반드시 model을 만들기 전에 schema 단에서
만들어야 한다.
middleware 안에서 this
는, 우리가 저장하고자 하는 document를 가리킨다.
이를 이용해 데이터가 데이터베이스에 저장되기 전에 document를 수정할 수 있다.
console 창에 middleware의 this 즉, document를 출력해보면, hashtags가 [ "a,b,c" ] 형태의 값을 가지고 있음을 알 수 있다.
schema에 hashtags 값을 string 배열 형태로 정의했기 때문에 사용자가 쉼표(,)로 구분해 입력한 여러 개의 hashtags들이 배열 내 하나의 요소로 들어간 것이다.
// Video.js
videoSchema.pre("save", async function() {
this.hashtags = this.hashtags[0].split(",").map(word => (word.startsWith("#") ? word : `#${word}`));
});
그러나, 이 hook은 Model.save()에서만 실행되고, Model.findByIdAndUpdate()에서는 실행되지 않는다.
두 가지 경우 모두에서 hashtags가 포맷되도록 해야 하므로 좀 더 유용한 다른 방법을 찾아봐야 한다.
💡 export 'formatHashtags 함수' & import
다른 방법으로 Video.js 파일에 hashtags를 포맷하는 함수를 작성하고 이를 export 한 후 videoController.js 파일에서 import 하는 방법이 있다.
// Video.js
export const formatHashtags = (hashtags) => hashtags.split(",").map((word) => (word.startsWith("#") ? word : `#${word}`));
// videoController.js
import Video, { formatHashtags } from "../models/Video"; // 수정 ❗
const postEdit = async (req, res) => {
const { id } = req.params;
const { title, description, hashtags } = req.body;
// 중략
await Video.findByIdAndUpdate(id, {
title,
description,
hashtags: formatHashtags(hashtags), // 수정 ❗
});
return res.redirect(`/videos/${id}`);
};
const postUpload = async (req, res) => {
const { title, description, hashtags } = req.body;
try {
await Video.create({
title,
description,
hashtags: formatHashtags(hashtags), // 수정 ❗
});
} catch(error) {
// 중략
}
return res.redirect("/");
};
이렇게 하면 save와 findByIdAndUpdate 할 때, 모두 hashtags가 포맷된다.
💡 statics
위 방법도 충분히 괜찮지만, 다른 방법도 있다.
findById()나 create() 같은 query를 직접 만들어 사용할 수 있다.
[ Video.js ]
import mongoose from "mongoose";
const videoSchema = mongoose.Schema({
// 중략
});
// 추가 ❗
videoSchema.static("formatHashtags", function(hashtags) {
return hashtags.split(",").map((word) => (word.startsWith("#") ? word : `#${word}`));
});
const Video = mongoose.model("Video", videoSchema);
[ videoController.js ]
import Video from "../models/Video"; // 수정 ❗
export const postEdit = async (req, res) => {
const { id } = req.params;
const { title, description, hashtags } = req.body;
// 중략
await Video.findByIdAndUpdate(id, {
title,
description,
hashtags: Video.formatHashtags(hashtags), // 수정 ❗
});
return res.redirect(`/videos/${id}`);
};
export const postUpload = async (req, res) => {
const { title, description, hashtags } = req.body;
try {
await Video.create({
title,
description,
hashtags: Video.formatHashtags(hashtags), // 수정 ❗
});
} catch(error) {
// 중략
}
return res.redirect("/");
};
이렇게 하면 export와 import 할 필요도 없이 save와 update 하기 전에 해시태그를 포맷할 수 있다.
💡 정리
- pre middleware - save만 ok
- export 포맷 함수 & import - save, update 모두 ok
- statics - save, update 모두 ok
watch.pug 파일에 video을 delete 하는 링크를 만든다.
//- watch.pug
extends base
block content
p=video.title
small=video.description
a(href='/videos/${video.id}`) Edit Video →
br
a(href=`/videos/${video.id}`) Delete Video →
// videoRouter.js
import express from "express";
import { ..., deleteVideo } from "../controllers/videoController";
const videoRouter = express.Router();
// 중략
videoRouter.route("/:id([0-9a-f]{24})/delete").get(deleteVideo); // 추가 ❗
export default videoRouter;
데이터를 삭제할 때는 기본적으로 post는 하지 않는다.
url을 방문하면 바로 video가 delete 되도록 한다.
// videoController.js
import Video from "../modules/Video";
// 중략
// 추가 ❗
export const deleteVideo = async (req, res) => {
const { id } = req.params;
await Video.findByIdAndDelete(id);
return res.redirect("/");
};
MongoDB는 강력한 필터 엔진을 가진다.
필요에 따라 데이터 정렬 방법과 검색 방법을 설정할 수 있다.
데이터베이스로부터 받아온 데이터들을 오름차순(asec) 또는 내림차순(desc)으로 정렬할 수 있다.
export const home = async (req, res) => {
try {
const videos = await Video.find({}).sort({ createdAt: "desc" }); // 내림차순
return res.render("home", { pageTitle: "Home", videos });
} catch {
return res.send("server-error");
}
};
위와 같이 작성하면, Home에 video가 만들어진 시간을 기준으로 내림차순으로 정렬된다.
import { ..., search } from "../controllers/videoController";
globalRouter.get("/search", search);
doctype html
html(lang="ko")
head
title #{pageTitle} | Wetube
link(rel="stylesheet" href="https://unpkg.com/mvp.css")
body
header
h1=pageTitle
nav
ul
li
a(href="/") Home
li
a(href=`/videos/upload`) Upload Video →
li
a(href="/search") Search Video → //- 추가 ❗
main
block content
include partials/footer
extends base
block content
form(method="GET")
input(name="keyword", placeholer="Search by keywords")
input(value="search", type="submit")
each video in videos
+video(video)
미리 videos 변수를 if 구문 밖에 선언하여 마지막에 렌더링할 때 videos가 빈 배열이어도 에러 없이 페이지가 표시되도록 한다.
export const search = async (req, res) => {
const { keyword } = req.query;
let videos = [];
if (keyword) {
videos = await Video.find({
title: keyword, // 검색 조건
});
}
return res.render("search", { pageTitle: "Search Video", videos });
};
💡 req.params / req.body / req.query
req.params
url의 파라미터 값req.body
form(method="POST")에 입력한 값
url에서 확인할 수 없다
JSON으로 데이터를 담을 때 사용req.query
form(method="GET")에 입력한 값
url에서 확인할 수 있다
아무런 요청도 안 하면 undefined 값을 가짐
MongoDB 정규 표현식을 이용하여 검색 조건을 좀 더 세련되게 설정할 수 있다.
export const search = async (req, res) => {
const { keyword } = req.query;
let videos = [];
if (keyword) {
videos = await Video.find({
title: {
$regex: new RegExp(keyword, "i"),
}
});
}
return res.render("search", { pageTitle: "Search", videos});
};
💡 mongoDB manual - Reference 中 Operators 참고
$regex: new RegExp(keyword, "i")
→ keyword(대소문자 구분 무시)를 포함하는
$regex: new RegExp(`^${keyword}`, "i")
→ keyword(대소문자 구분 무시)로 시작하는
$regex: new RegExp(`${keyword}$`, "i")
→ keyword(대소문자 구분 무시)로 끝나는
+신세계였던 MongoDB and Mongoose 챕터(정확히는 video 파트)가 끝났다! 😁