[TIL] 211127

Lee Syong·2021년 11월 27일
0

TIL

목록 보기
101/204
post-thumbnail

📝 오늘 한 것

  1. mongoose로 CRUD operation 구현하기 - edit video / delete video / search video

  2. Model.exists() & Model.findByIdAndUpdate() / pre middleware / statics / 데이터 정렬 / req.query / mongoDB 정규 표현식


📚 배운 것

CRUD

1. edit video

1) edit.pug 수정

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")

2) getEdit 컨트롤러

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 });
};

3) postEdit 컨트롤러

(1) 방법 1: find → update → save

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}`);
};

(2) 방법 2: exists() & findByIdAndUpdate()

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}`);
};

(3) 해시태그 포맷

💡 mongoose middleware

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, // 수정
});
  • 일단, videoController.js 파일의 postEidt 컨트롤러에서 hashtags 값을 깔끔하게 그냥 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}`));
});
  • 원래대로 각 hashtag 앞에 #을 추가하기 위해 위와 같이 코드를 작성할 수 있다.

그러나, 이 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 ]

  • Video.js 파일에 static을 추가한다.
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 ]

  • videoController.js 파일에서 static을 사용한다.
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

2. delete video

1) watch.pug 수정

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 →

2) videoRouter.js 파일 수정

// 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;

3) deleteVideo 컨트롤러

데이터를 삭제할 때는 기본적으로 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("/");
};

3. search video

MongoDB는 강력한 필터 엔진을 가진다.
필요에 따라 데이터 정렬 방법과 검색 방법을 설정할 수 있다.

1) 데이터 정렬 방법 선택

데이터베이스로부터 받아온 데이터들을 오름차순(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가 만들어진 시간을 기준으로 내림차순으로 정렬된다.

2) 검색 페이지 만들기

(1) videoRouter.js 파일 수정

import { ..., search } from "../controllers/videoController";

globalRouter.get("/search", search);

(2) base.pug 파일 수정

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

(3) search.pug 파일 생성

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)

(4) videoController.js 파일 수정

미리 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 값을 가짐

(5) 검색 조건 개선

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 파트)가 끝났다! 😁


✨ 내일 할 것

  1. 강의 처음부터 지금까지 배운 부분 빠르게 복습하기
profile
능동적으로 살자, 행복하게😁

0개의 댓글