[TIL] 211217

Lee Syong·2021년 12월 17일
0

TIL

목록 보기
121/204
post-thumbnail

📝 오늘 한 것

  1. 댓글 DB에 저장하기 / 실시간으로 댓글 보여주기

📚 배운 것

1. comments section

1) commentSchema 작성

models 폴더 안에 Comment.js 파일을 만들어 commentSchema를 작성하고 model들 간의 관계를 고려하여 userSchema와 videoSchema를 수정한다.

  • user는 댓글을 달 수 있다 = 댓글은 user를 가진다.
  • user는 여러 개의 댓글을 달 수 있다 = user는 댓글 array를 가진다.
  • 댓글은 video에 달린다 = 댓글은 video를 가진다.
  • video에 여러 개의 댓글이 달릴 수 있다 = video는 댓글 array를 가진다.

comment model을 만들어 export 한 후 init.js 파일에서 import 한다.

import mongoose from "mongoose";

const commentSchema = new mongoose.Schema({
  text: { type: String, required: true },
  createdAt: { type: Date, required: true, default: Date.now },
  owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
  video: { type: mongoose.Schema.Types.ObjectId, ref: "Video", required: true },
});

const Comment = mongoose.model("Comment", commentSchema);

export default Comment;
// init.js
import "./models/Comment";

2) 프론트엔드 댓글 내용 백엔드로 가져오기

(1) webpack 설정

client 폴더 안에 commentSection.js 파일을 만들고 webpack.config.js 파일을 수정한 후 webpack을 다시 시작한다.

(2) HTML 마크업

watch.pug 파일에서 commentSection.js 파일을 로드할 수 있도록 script를 추가한 후 댓글 작성란을 만든다.
로그인 했을 때만 script를 다운로드 하고 댓글 작성란을 볼 수 있도록 한다.

//- watch.pug

extends base

block scripts
  script(src="/static/js/videoPlayer.js")
  if loggedIn
    script(src="/static/js/commentSection.js")

block content
  //- 중략

  if loggedIn
    div.video-comments
      form.video-comments__form#commentForm
        textarea(cols="30", rows="10", placeholder="Write a nice comment...")
        button Add Comment

(3) commentSection.js 파일 작성

user가 작성한 댓글 내용을 가져온다.
어떤 video에 댓글을 추가해야 하는지 알기 위해 댓글이 달린 video를 가져온다.
댓글 내용과 댓글이 달린 비디오를 가져왔으면, 이제 fetch를 이용해 백엔드에 post request를 보내야 한다

// commentSection.js
const videoContainer = document.getElementById("videoContainer");
const form = document.getElementById("commentForm");
const textarea = form.querySelector("textarea");

const handleSubmit = (event) => {
  event.preventDefault();
  const text = textarea.value;
  if (text === "") {
    return;
  }
  const videoId = videoContainer.dataset.id;
  
  // fetch 이용...
  
  textarea.value = "";
};

form.addEventListener("submit", handleSubmit);

fetch를 이용해 url 변경 없이 post request를 보내기 위해 apiRouter.js 파일에 route를 추가한다.

// apiRouter.js
import { createComment } from "../controllers/videoController";

apiRouter.post("/videos/:id([0-9a-f]{24})/comment", createComment);

user가 작성한 댓글 내용은 req.body를 통해 백엔드(createComment 컨트롤러)에 가져와 사용할 수 있다.
req.body는 앞서 다뤄봤던 form 뿐만이 아니라 fetch를 통해서도 만들 수 있다.

// commentSection.js
const videoContainer = document.getElementById("videoContainer");
const form = document.getElementById("commentForm");
const textarea = form.querySelector("textarea");

const handleSubmit = (event) => {
  event.preventDefault();
  const text = textarea.value;
  if (text === "") {
    return;
  }
  const videoId = videoContainer.dataset.id;
  fetch(`/api/videos/${videoId}/comment`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json", // 2-1. express에게 request의 Content-type이 json이라는 것을 알려주기 위해 추가함
    },
    body: JSON.stringify({ text }), // 1. 프론트엔드에서 JS Object를 JSON 문자열로 바꾼 후 fetch를 이용해 백엔드로 보낸다.
  });
  textarea.value = "";
};

form.addEventListener("submit", handleSubmit);

앞서 서버가 form에서 만들어진 body를 이해할 수 있도록, express의 body-parser 미들웨어를 사용했었다.
마찬가지로, 서버가 fetch를 통해 만들어진 body도 이해할 수 있도록, JSON 문자열을 JS Object로 바꿔주는 express의 미들웨어를 사용해야 한다.

// server.js
app.use(express.urlencoded({ extended: true }));
app.use(express.json()); // 2-2. 백엔드에서는 express가 프론트엔드로부터 받은 JSON 문자열을 다시 JS Object로 바꾼다.

이를 통해 백엔드에서 req.body를 이용해 프론트엔드에서 보낸 데이터를 가져와 쓸 수 있다.

// videoController.js
export const createComment = (req, res) => {
  console.log(req.body.text); // 3. createComment 컨트롤러에서 req.body.text를 이용해 댓글 내용을 가져올 수 있다.
  return res.end();
};

💡 프론트엔드 - fetch - apiRouter - 백엔드(컨트롤러)

3) comments DB에 댓글 저장하기

(1) 댓글을 작성한 user 가져오기

이제 백엔드에서는 댓글 내용(req.body)과 댓글이 작성된 비디오(req.params)에 대한 정보를 사용할 수 있다.
댓글을 표시하기 위해서는 마지막으로 댓글을 작성한 user에 대한 정보를 session을 이용해 가져와야 한다.
(이는 보안상 중요한 문제이므로 프론트엔드에서 다루지 않고 백엔드에서 다뤄야 한다.)

프론트엔드에서 fetch를 이용해 백엔드로 request를 보낼 때는 항상 세션 id와 함께 보내진다.
이는 이미 로그인할 때 서버로부터 받은 것으로써 그 이후로 브라우저가 서버에 요청을 보낼 때마다 항상 같이 보내지는 것이다.
서버는 그 값을 세션 DB 안의 값과 비교해 어떤 user가 요청을 보냈는지를 파악할 수 있다.
다시 말해, 처음 로그인 시 (postLogin 컨트롤러에서) 만들어졌던 req.session.user의 값을 createComment 컨트롤러에서도 사용할 수 있다.

(2) createComment 컨트롤러 작성

// videoController.js
import Comment from "../models/Comment";

export const createComment = async (req, res) => {
  const {
    session: { user },
    params: { id },
    body: { text },
  } = req;
  const video = await Video.findById(id);
  if (!video) {
    return res.sendStatus(404);
  }
  await Comment.create({
    text,
    owner: user._id,
    video: id,
  });
  return res.sendStatus(201); // '생성됨'을 뜻하는 상태 코드
};

이제 textarea에 내용 작성 후 버튼을 누르면 comments DB에 저장된다.

이때 textarea에서 내용 작성 시 Spacebar나 Enter를 누르면 video가 재생되는 문제가 있어서 handleKeyDown 함수를 수정했다.

// videoPlayer.js
const handleKeyDown = (event) => {
  // 추가 ❗
  const el = document.activeElement;
  if ((el && el.selectionStart !== undefined) || el.isContentEditable) {
    return;
  }
 
  const { key } = event;
  const { currentTime } = video;
  switch (key) {
    case " ":
      handlePlayOrPause();
      event.preventDefault();
      break;
    case "ArrowLeft":
      video.currentTime = currentTime - 1;
      event.preventDefault();
      break;
    case "ArrowRight":
      video.currentTime = currentTime + 1;
      event.preventDefault();
      break;
    case "Enter":
      handleFullscreen();
      break;
  }
};

4) videos DB로부터 댓글 가져와 보여주기

(1) videos DB에 댓글 저장하기

위에서는 comments DB에만 댓글을 저장했다.
watch 템플릿에서 보내는 video를 이용해 댓글을 보여주기 위해서는 videos DB에도 댓글을 저장해야 한다.

// videoController.js
export const createComment = async (req, res) => {
  const {
    session: { user },
    body: { text },
    params: { id },
  } = req;
  const video = await Video.findById(id);
  if (!video) {
    return res.sendStatus(404);
  }
  const comment = await Comment.create({
    text,
    owner: user._id,
    video: id,
  });
  // video.comments에 새로 만든 댓글을 업데이트 한다
  video.comments.push(comment);
  await video.save();
  return res.sendStatus(201);
};

앞서 watch 페이지에서 user 정보를 사용하기 위해 populate("owner")를 사용했다.
마찬가지로 watch 페이지에서 comments 정보를 사용하기 위해 populate("comments")를 사용한다.

// videoController.js
export const watch = async (req, res) => {
  const { id } = req.params;
  const video = await Video.findById(id).populate("owner").populate("comments");
  if (!video) {
    return res.render("404", { pageTitle: "Video not found" });
  }
  return res.render("watch", { pageTitle: video.title, video });
};

이제 watch 컨트롤러에서 watch.pug 파일에 video를 보내고 있으므로 video.comments를 이용해 댓글을 프론트엔드로 가져올 수 있다.

(2) 댓글을 화면에 보여주기

watch.pug 파일에 댓글을 보여줄 공간을 추가한다.
최신순으로 보여주기 위해 reverse()를 붙였다. (pug는 자바스크립트 코드를 즉시 실행한다.)

div.video-comments
  ul
    each comment in video.comments.reverse()
      li.video-comment
        i.fas.fa-comment
        |  #{comment.text}

watch.scss 파일에 CSS 코드를 추가한다.

.video-comments {
  display: flex;
  flex-direction: column;
  max-width: 320px;
  margin: 0 auto;
  margin-top: 40px;
  .video-comment {
    background-color: white;
    border-radius: 2em; // px로 지정하면 텍스트가 잘림
    color: $bg;
    padding: 10px 20px;
    margin-bottom: 10px;
    line-height: 1.5; // 줄 간격 설정
    overflow-wrap: break-word; // 컨테이너 너비 끝에 다다르면 무조건 줄 바꿈
  }
}

이제 textarea에 댓글을 작성하고 '새로고침을 하면' 댓글이 최신순으로 화면에 보여진다.

5) 실시간으로 댓글 보여주기

(1) async & await / 201 상태 코드

우선, fetch가 백엔드에 갖다가 돌아오기까지 다음 코드가 진행되지 않도록 async & await을 추가한다.
한편, fetch는 돌아오긴 돌아오지만 꼭 성공하는 것은 아니다.

fetch가 return 하는 응답에는 status 값이 있다.
이를 이용해 createComment 컨트롤러에서 video가 존재하지 않아 에러가 발생한 경우는 제외하고, 백엔드가 201 상태 코드를 보내준 경우에만 댓글을 보여주도록 한다.

// commentSection.js
const handleSubmit = async (event) => {
  event.preventDefault();
  const text = textarea.value;
  if (text === "") {
    return;
  }
  const videoId = videoContainer.dataset.id;
  const { status } = await fetch(`/api/videos/${videoId}/comment`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ text }),
  });
  textarea.value = "";
  if (status === 201) {
    // JavaScript로 가짜 댓글을 만든다...
  }
};

(2) JavaScript로 댓글 만들기

새로고침을 하지 않아도 새로 작성한 댓글이 실시간으로 보이도록 pug 파일에 만들어준 댓글란과 똑같은 구조를 가지는 가짜 댓글을 만들려고 한다.

//- watch.pug

div.video-comments
  ul
    each comment in video.comments.reverse()
      li.video-comment
        i.fas.fa-comment
        span  #{comment.text}
// commentSection.js
const addComment = (text) => {
  const videoComments = document.querySelector(".video-comments ul"); // ul만 querySelector로 가져오고
  const newComment = document.createElement("li"); // 새로운 댓글은 createElement로 만듦 (querySelector 사용하면 덮어쓰기 됨)
  newComment.className = "video-comment";
  const icon = document.createElement("i");
  icon.className = "fas fa-comment";
  const span = document.createElement("span");
  span.textContent = ` ${text}`;
  newComment.appendChild(icon);
  newComment.appendChild(span);
  videoComments.prepend(newComment); // 새로운 댓글이 ul에 계속 쌓이도록 함
};

이제 textarea에 댓글을 작성하면 JavaScript 코드에 의해 실시간으로 화면에 보이게 된다.

이때 JavaScript로 실시간으로 나타나는 댓글을 구현했다 하더라도 pug 파일에 만들어준 댓글란 코드를 지워서는 안된다.
JavaScript로는 말 그대로 실시간으로 댓글을 보여줄 뿐이라서 pug 파일에 있는 코드를 지우면 새로고침 했을 때 댓글이 화면에서 전부 사라져버리기 때문이다.


✨ 내일 할 것

  1. 댓글 삭제하기
profile
능동적으로 살자, 행복하게😁

0개의 댓글