[TIL] 211212

Lee SyongΒ·2021λ…„ 12μ›” 12일
0

TIL

λͺ©λ‘ 보기
116/204
post-thumbnail

πŸ“ 였늘 ν•œ 것

  1. video & videoControls - CSS μŠ€νƒ€μΌλ§ 및 μ •λ ¬

  2. keydown 이벀트

  3. API - apiRouter / sendStatus / fetch API / data attribute


πŸ“š 배운 것

1. video player

CSS μŠ€νƒ€μΌλ§ 및 μ •λ ¬ + video player 마무리
λ§‰ν˜”λ‹€κ°€ ν•΄κ²°ν•œ λΆ€λΆ„λ“€ ν˜Ήμ€ λ‹€μ‹œ 보고 싢은 λΆ€λΆ„λ“€ 정리

1) video 쀑앙 μ •λ ¬

videoλŠ” inline 레벨 μš”μ†Œμ΄λ‹€.

μ—¬νƒœκΉŒμ§€ block 레벨 μš”μ†Œλ‘œ μ•Œκ³  μžˆμ—ˆλ˜ κ±° 싀화인가. ν•œκ΅­μ–΄λ‘œ κ΅¬κΈ€λ§ν•˜λ©΄ ν¬μŠ€νŒ…λ§ˆλ‹€ videoκ°€ block 레벨 μš”μ†ŒλΌκ³  μ„€λͺ…λ˜μ–΄ μžˆλ‹€. MDNμ—μ„œ ν™•μΈν•΄λ³΄λ‹ˆ inline 레벨 μš”μ†ŒλΌκ³  μ ν˜€ μžˆμ—ˆλ‹€.

video {
  display: block;
  margin: 0 auto;
}

2) ν°λ”°μ˜΄ν‘œ / μž‘μ€λ”°μ˜΄ν‘œ

ν°λ”°μ˜΄ν‘œ μ•ˆμ—λŠ” μž‘μ€λ”°μ˜΄ν‘œλ₯Ό 써야 ν•œλ‹€. μžŠμ§€ 말자.

3) videoControls opacity

'showing' classκ°€ μžˆμ„ λ•Œλ§Œ λΉ„λ””μ˜€ μ»¨νŠΈλ‘€λŸ¬κ°€ 보이도둝 ν•΄μ•Ό ν•œλ‹€.
κ·ΈλŸ¬λ‚˜, μ•„λž˜μ²˜λŸΌ μž‘μ„±ν•˜λ©΄ λΉ„λ””μ˜€ μ»¨νŠΈλ‘€λŸ¬λŠ” μ–΄λ–»κ²Œ 해도 보이지 μ•ŠλŠ”λ‹€.

#videoControls {
  opacity: 0;
}

.showing {
  opacity: 1;
}

μ΄λ ‡κ²Œ μž‘μ„±ν•΄μ•Ό ν•œλ‹€.

#videoControls {
  opacity: 0;
  &.showing {
    opacity: 1;
  }
}

4) video-player.scss

watch.pug 파일과 videoPlayer.js νŒŒμΌμ—μ„œ λ²„νŠΌ μ•ˆμ˜ ν…μŠ€νŠΈλ“€μ„ font-awesome μ•„μ΄μ½˜μœΌλ‘œ λ°”κΏ¨λ‹€.
videoContainer(video & videoControls)λ₯Ό μŠ€νƒ€μΌλ§ν•˜κ³  μ •λ ¬ν–ˆλ‹€.

#videoContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
}

video {
  width: 100%;
  max-height: 800px; // μ„Έλ‘œκ°€ κΈ΄ λ™μ˜μƒμ„ μœ„ν•΄ 좔가함
}

#videoControls {
  opacity: 0;
  &.showing {
    opacity: 1;
  }
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 98%;
  padding-bottom: 1%;
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translate(-50%, -100%);
  background-color: transparent;
  button {
    all: unset;
    cursor: pointer;
  }
  #volume {
    width: 10%;
    &:focus {
      outline: none;
    }
  }
  #timeline {
    width: 65%;
    &:focus {
      outline: none;
    }
  }
}

5) click & keydown

πŸ’‘ ν‚€λ³΄λ“œ ν‚€λ₯Ό λˆ„λ₯΄λ©΄ keydown β†’ keypress β†’ keyup의 μˆœμ„œλŒ€λ‘œ μ΄λ²€νŠΈκ°€ λ°œμƒν•œλ‹€.

μ΄λ•Œ keydown μ΄λ²€νŠΈλŠ” ν‚€λ³΄λ“œμ˜ λͺ¨λ“  ν‚€λ₯Ό λˆŒλ €μ„ λ•Œ λ°œμƒν•˜λŠ” 반면, keypress μ΄λ²€νŠΈλŠ” 좜λ ₯ν•  수 μžˆλŠ” ν‚€λ³΄λ“œλ₯Ό λˆŒλ €μ„ λ•Œλ§Œ λ°œμƒν•œλ‹€.(ν™”μ‚΄ν‘œ 등을 λˆŒλ €μ„ λ•ŒλŠ” keypress 이벀트 λ°œμƒx)

(1) μž¬μƒ / μΌμ‹œμ •μ§€

playBtnκ³Ό videoλ₯Ό ν΄λ¦­ν•˜κ±°λ‚˜ Space λ°”λ₯Ό λˆ„λ₯΄λ©΄, videoκ°€ μž¬μƒ λ˜λŠ” μΌμ‹œμ •μ§€λ˜λ„λ‘ μˆ˜μ •ν–ˆλ‹€.
space λ°”λ₯Ό λˆ„λ₯Ό λ•Œλ§ˆλ‹€ 슀크둀이 λ‚΄λ €κ°€λŠ” 것(λΈŒλΌμš°μ €μ˜ κΈ°λ³Έ λ™μž‘)을 막기 μœ„ν•΄ event.preventDefault()λ₯Ό μ‚¬μš©ν–ˆλ‹€.

const handlePlayOrPause = () => {
  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
  playBtn.innerHTML = video.paused
    ? "<i class='fas fa-play'></i>"
    : "<i class='fas fa-pause'></i>";
};

const handleKeyPress = (event) => {
  if (event.code === "Space") {
    handlePlayOrPause();
    event.preventDefault()
  }
};

playBtn.addEventListener("click", handlePlayOrPause);
video.addEventListener("click", handlePlayOrPause);
window.addEventListener("keypress", handleKeyPress);

(2) ν˜„μž¬ μž¬μƒ μ‹œκ°„ λ³€κ²½

ArrowLeft와 ArrowRightλ₯Ό λˆ„λ₯΄λ©΄ λ™μ˜μƒ ν˜„μž¬ μž¬μƒ μœ„μΉ˜λ₯Ό 이동할 수 μžˆλ„λ‘ μˆ˜μ •ν–ˆλ‹€.
handleKeyPress ν•¨μˆ˜λ₯Ό swith ꡬ문과 event.keyλ₯Ό μ΄μš©ν•΄ μž‘μ„±ν–ˆλ‹€.

const handleKeyPress = (event) => {
  const { key } = event;
  const { currentTime } = video;
  switch (key) {
    case " ":
      handlePlayOrPause();
      event.preventDefault();
      break;
    case "ArrowLeft":
      video.currentTime = currentTime - 1;
      break;
    case "ArrowRight":
      video.currentTime = currentTime + 1;
  }
};

window.addEventListener("keydown", handleKeyPress);

(3) 전체 ν™”λ©΄ μ „ν™˜

전체 ν™”λ©΄ λ²„νŠΌμ„ ν΄λ¦­ν–ˆμ„ λ•ŒλΏ μ•„λ‹ˆλΌ Enter ν‚€λ₯Ό λˆŒλŸ¬λ„ 전체 ν™”λ©΄μœΌλ‘œ μ „ν™˜ν•  수 μžˆλ„λ‘ μˆ˜μ •ν–ˆλ‹€.

const handleFullscreen = () => {
  const fullscreen = document.fullscreenElement;
  if (fullscreen) {
    document.exitFullscreen();
    fullScreenBtn.innerHTML = "<i class='fas fa-expand'></i>";
  } else {
    videoContainer.requestFullscreen();
    fullScreenBtn.innerHTML = "<i class='fas fa-compress'></i>";
  }
};

const handleKeyPress = (event) => {
  const { key } = event;
  const { currentTime } = video;
  switch (key) {
    case " ":
      handlePlayOrPause();
      event.preventDefault();
      break;
    case "ArrowLeft":
      video.currentTime = currentTime - 1;
      break;
    case "ArrowRight":
      video.currentTime = currentTime + 1;
      break;
    case "Enter":
      handleFullscreen();
      break;
  }
};

fullScreenBtn.addEventListener("click", handleFullscreen);
window.addEventListener("keydown", handleKeyPress);

2. API

μ•žμ„œ λ°±μ—”λ“œλ₯Ό λ‹€λ£° λ•Œ pugλ₯Ό μ‚¬μš©ν•΄μ„œ NodeJS둜 ν…œν”Œλ¦Ώ(views)을 λ Œλ”λ§ν•˜λŠ” SSR 방식을 μ‚¬μš©ν–ˆλ‹€.
SSR(Server Side Rendering)μ΄λž€ μ„œλ²„κ°€ ν…œν”Œλ¦Ώμ„ λ Œλ”λ§ν•˜λŠ” μΌκΉŒμ§€ ν•˜λŠ” 것을 μ˜λ―Έν•œλ‹€.

κ·ΈλŸ¬λ‚˜, λ°±μ—”λ“œμ—μ„œ ν…œν”Œλ¦Ώμ„ λ Œλ”λ§ν•˜μ§€ μ•Šκ³  μš”μ¦˜μ€ APIλ₯Ό μ΄μš©ν•œλ‹€.
APIλž€ λ°±μ—”λ“œκ°€ ν…œν”Œλ¦Ώμ„ λ Œλ”λ§ν•˜μ§€ μ•Šμ„ λ•Œ ν”„λ‘ νŠΈμ—”λ“œμ™€ λ°±μ—”λ“œκ°€ μ„œλ²„λ₯Ό 톡해 ν†΅μ‹ ν•˜λŠ” 방법을 λ§ν•œλ‹€.

userκ°€ video μ‹œμ²­μ„ ν•œλ²ˆ μ™„λ£Œν•  λ•Œλ§ˆλ‹€, μ‘°νšŒμˆ˜κ°€ μ—…λ°μ΄νŠΈλ˜λ„λ‘ APIλ₯Ό μ΄μš©ν•΄ κ΅¬ν˜„ν•΄λ³΄λ €κ³  ν•œλ‹€.

1) apiRouter

apiRouter.js νŒŒμΌμ„ λ§Œλ“  ν›„ apiRouterλ₯Ό λ§Œλ“€μ–΄ export default ν•œλ‹€.
server.js νŒŒμΌμ—μ„œ apiRouterλ₯Ό import ν•œ ν›„ 이λ₯Ό μ‚¬μš©ν•  수 μžˆλ„λ‘ μΆ”κ°€ν•œλ‹€.

// apiRouter.js
import express from "express";

const apiRouter = express.Router();

export default apiRouter;
// server.js
import apiRouter from "./routers/apiRouter";

app.use("/api", apiRouter);

apiRouter.js νŒŒμΌμ—μ„œ routeλ₯Ό μΆ”κ°€ν•œλ‹€.
userκ°€ videoλ₯Ό μ‹œμ²­ν•˜κΈ° μ‹œμž‘ν•˜λ©΄ λ°±μ—”λ“œμ— post μš”μ²­μ„ 보내도둝 ν•œλ‹€.

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

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

2) registerView 컨트둀러

registerView 컨트둀러λ₯Ό μž‘μ„±ν•œλ‹€.
req.params.idλ₯Ό μ΄μš©ν•΄ DBμ—μ„œ videoλ₯Ό 찾은 ν›„ video.meta.viewsλ₯Ό μ—…λ°μ΄νŠΈν•œλ‹€.

μ΄λ•Œ url을 λ°”κΏ” νŽ˜μ΄μ§€λ₯Ό μ΄λ™ν•˜μ§€λŠ” μ•Šμ„ 것이닀. (즉, ν…œν”Œλ¦Ώμ„ λ Œλ”λ§ν•˜μ§€ μ•Šμ„ 것이닀.)
κ·ΈλŸ¬λ‚˜ ν…œν”Œλ¦Ώμ€ λ Œλ”λ§ν•˜μ§€ μ•Šμ•„λ„ μ„œλ²„κ°€ μš”μ²­μ— λŒ€ν•œ 응닡은 ν•΄μ•Ό ν•˜λ―€λ‘œ render λŒ€μ‹  sendStatusλ₯Ό μ΄μš©ν•΄ status codeλ₯Ό 보내도둝 ν•œλ‹€.

πŸ’‘ statusλŠ” render 전에 μƒνƒœ μ½”λ“œλ₯Ό 지정해쀄 λΏμ΄μ§€λ§Œ, sendStatusλŠ” μ§€μ •ν•œ μƒνƒœ μ½”λ“œλ₯Ό λ³΄λƒ„μœΌλ‘œμ¨ 연결을 끝내버린닀.

// videoController.js
export const registerView = async (req, res) => {
  const { id } = req.params;
  const video = await Video.findById(id);
  if (!video) {
    return res.sendStatus(404);
  }
  video.meta.views = video.meta.views + 1;
  await video.save();
  return res.sendStatus(200);
};

πŸ’‘ μƒνƒœ μ½”λ“œ

μƒνƒœ μ½”λ“œλŠ” κΈ°λŠ₯이 μ œλŒ€λ‘œ μ²˜λ¦¬λ˜μ—ˆλŠ”μ§€λ₯Ό μ•Œλ €μ€€λ‹€.

쑰회수λ₯Ό μ—…λ°μ΄νŠΈν•  λ•ŒλŠ” μƒνƒœ μ½”λ“œλ₯Ό 지정해주긴 ν•˜μ§€λ§Œ 이λ₯Ό ꡳ이 μ΄μš©ν•˜μ§€λŠ” μ•Šμ„ 것이닀. μ‚¬μš©μžμ—κ²Œ 'μ‘°νšŒμˆ˜κ°€ μ˜¬λΌκ°€μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'라고 μ•Œλ €μ€„ ν•„μš”λŠ” μ—†κΈ° λ•Œλ¬Έμ΄λ‹€.

κ·ΈλŸ¬λ‚˜, λ’€μ—μ„œ λŒ“κΈ€μ„ λ‹€λ£° λ•ŒλŠ” μƒνƒœ μ½”λ“œλ₯Ό μ΄μš©ν•  것이닀. μ‚¬μš©μžμ—κ²Œ 'λŒ“κΈ€ μž‘μ„±μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'라고 μ•Œλ €μ£Όκ±°λ‚˜ μΆ”κ°€λœ λŒ“κΈ€μ„ λ³Ό 수 μžˆλ„λ‘ λ§Œλ“€μ–΄μ€˜μ•Ό ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

3) ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ νŽ˜μ΄μ§€ 이동 없이 url 호좜

(1) fetch API

μœ„μ—μ„œ 쑰회수λ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” μ½”λ“œλ₯Ό μž‘μ„±ν–ˆλ‹€.
이제 userκ°€ video μ‹œμ²­μ„ ν•œλ²ˆ μ™„λ£Œν•  λ•Œλ§ˆλ‹€ ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ μœ„μ—μ„œ μž‘μ„±ν•œ url을 ν˜ΈμΆœν•˜λ„λ‘ ν•΄μ•Ό ν•œλ‹€.

μ•žμ„œ λ°±μ—”λ“œλ₯Ό λ‹€λ£° λ•ŒλŠ” λΈŒλΌμš°μ €κ°€ μ„œλ²„μ— μš”μ²­μ„ ν•˜μ—¬ url이 λ°”λ€Œλ©΄(νŽ˜μ΄μ§€κ°€ μ΄λ™λ˜λ©΄) μ„œλ²„κ°€ 응닡을 ν•˜λŠ” λ°©μ‹μ΄μ—ˆλ‹€.
μ΄λ²ˆμ—λŠ” ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ νŽ˜μ΄μ§€ 이동 없이 url을 ν˜ΈμΆœν•΄λ³Ό 것이닀.
이λ₯Ό 톡해 url이 λ°”λ€Œμ§€ μ•Šμ•„λ„ νŽ˜μ΄μ§€μ— λ³€ν™”κ°€ μΌμ–΄λ‚˜λŠ” interactive websiteλ₯Ό λ§Œλ“€ 수 μžˆλ‹€.

// videoPlayer.js
const handleVideoEnded = () => {
  // μ€‘λž΅

  // fetch("/api/videos/video의 id/view");
};

video.addEventListener("ended", handleVideoEnded); 

λ¨Όμ €, λΉ„λ””μ˜€κ°€ λλ‚˜λŠ” 것을 κ°μ§€ν•˜λŠ” ended 이벀트λ₯Ό λ“±λ‘ν•œ ν›„ 이벀트 λ¦¬μŠ€λ„ˆμ—μ„œ fetch()λ₯Ό μ΄μš©ν•΄ μ•žμ—μ„œ λ§Œλ“  route에 μš”μ²­μ„ 보내도둝 ν•˜λ €κ³  ν•œλ‹€.
그런데 api둜 μš”μ²­μ„ 보내기 μœ„ν•΄μ„œλŠ” videoPlayer.jsμ—μ„œ video.id의 값을 κ°€μ Έμ˜¬ 수 μžˆμ–΄μ•Ό ν•œλ‹€.

(2) ν”„λ‘ νŠΈμ—”λ“œμ™€ λ°±μ—”λ“œμ—μ„œ 데이터 κ³΅μœ ν•˜λŠ” 법

이λ₯Ό μœ„ν•΄ data 속성을 μ΄μš©ν•΄ 직접 μ»€μŠ€ν…€ 데이터λ₯Ό λ§Œλ“€μ–΄ HTML element에 μ €μž₯ν•˜λ €κ³  ν•œλ‹€.
ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ μ•Œ 수 μžˆλ„λ‘ pug νŒŒμΌμ— video.id의 값을 남길 수 μžˆλ‹€.

//- watch.pug

// μ€‘λž΅
video(src="/" + video.fileUrl, data-id=video._id)

pug νŒŒμΌμ—μ„œ data-λ₯Ό μ΄μš©ν•΄ μ»€μŠ€ν…€ 데이터λ₯Ό μ €μž₯ν•œλ‹€.

// videoPlayer.js
const handleVideoEnded = () => {
  // μ€‘λž΅
  
  const { id } = video.dataset;
  fetch(`/api/videos/${id}/view`, {
    method: "POST",
  })
};

video.addEventListener("ended", handleVideoEnded);

ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ element.dataset을 μ΄μš©ν•΄ μ €μž₯ν•œ 데이터λ₯Ό κ°€μ Έμ˜¨λ‹€.
fetch()λŠ” 기본적으둜 GET Requestλ₯Ό λ³΄λ‚΄κ²Œ λ˜λ―€λ‘œ 이λ₯Ό POST Request둜 λ°”κΏ”μ•Ό ν•œλ‹€.

이제 아무것도 ν΄λ¦­ν•˜μ§€ μ•Šκ³  url을 바꾸지 μ•Šμ•„λ„ μ§€μ •ν•œ μ΄λ²€νŠΈκ°€ λ°œμƒν•˜κΈ°λ§Œ ν•˜λ©΄ ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ μ•Œμ•„μ„œ λ°±μ—”λ“œμ— μš”μ²­μ„ 보낸닀.


✨ 내일 ν•  것

  1. video recorder
profile
λŠ₯λ™μ μœΌλ‘œ μ‚΄μž, ν–‰λ³΅ν•˜κ²ŒπŸ˜

0개의 λŒ“κΈ€