user ννΈ λ€μ΄κ°κΈ° μ μ μ¬νκΉμ§ λ°°μ΄ λ΄μ©(node.js ~ video ννΈ) λ€μ 볡μ΅ν¨
ν·κ°λ¦¬κ±°λ λ€μ λ΄€μΌλ©΄ μ’μ κ² κ°μ λΆλΆμ μμΈνκ², λλ¨Έμ§ λΆλΆμ μ μ²΄ νλ¦μ μ΅νκΈ° μν΄ κ°λ΅νκ² μ λ¦¬νλ μμΌλ‘ μμ±ν¨
git init / npm init
ν°λ―Έλμμ package.json νμΌμ΄ λ€μ΄ μλ ν΄λλ‘ κ° ν npm i expressμ μ
λ ₯νλ€.
npm i (package.json νμΌ λ°λμ μ μ₯ ν λ«κ³ )
babel / nodemon μ€μΉ [TIL] 211113 μ°Έκ³
import express from "express";
const PORT = 4000;
const app = express();
const handleListening = () => console.log("μλ²κ° μ΄λ Έλ€");
app.listin(PORT, handleListening);
'app λ§λ€κΈ°'μ 'μλ² μ΄κΈ°' μ¬μ΄μ μμΉνλ€.
appμ λ§λ  ν 'μ€λΉκ° μλ£λλ©΄' μλ²λ₯Ό μ¬λ κ²μ΄λ€.
app.get("route", handler ν¨μ);
requestμ response μ¬μ΄μμ requestλ₯Ό μ§μμν€λ ν¨μμ΄λ€.
expressκ° requestλ₯Ό νμΈνλ©΄ μμλλ‘ handler ν¨μλ₯Ό νΈμΆνλλ°
λ³Έλ¬Έμ next()κ° μμΌλ©΄ κ·Έ ν¨μλ handlerμ΄μ middlewareμ΄λ€.
const middleware = (req, res, next) => {
  console.log("μ΄κ±΄ λ―Έλ€μ¨μ΄λ€");
  next();
};
const handleHome  = (req, res, next) => {
  return res.end();
};
app.get("route", middleare, handleHome);
global middlewareλ₯Ό λ§λ€μ΄μ€λ€.
app.use()λ app.get()λ³΄λ€ μ΄μ μ μμΉν΄μΌ νλ€.
const middleware = (req, res, next) => {
  console.log("μ΄κ±΄ λͺ¨λ  μμ²μ λν΄ μλ²κ° μλ΅νκΈ° μ μ μ€νλλ κΈλ‘λ² λ―Έλ€μ¨μ΄λ€");
  next();
};
const handleHome = (req, res, next) => {
  return res.end();
};
app.use(globalMiddleware);
app.get("/", handleHome);
보νΈλ νμ΄μ§μ μ κ·Όν μ μκ²λ ν΄μ€λ€.
const privateMiddleware = (req, res, next) => {
  const url = req.url;
  if (url === '/protected') {
    return res.send("<h1>보νΈλ νμ΄μ§λΌ μ κ·Όν  μ μμ.</h1>");
  }
  next();
};
const handleProtected = (req, res, next) => {
  return res.send("μ¬κΈ°λ 보νΈλ νμ΄μ§ μ
λλ€. νμν©λλ€.");
};
app.use(privateMiddleware);
app.get("/protected", handleProtected);
μ½μ μ°½μ loggerλ₯Ό 보μ¬μ€
import morgan from "morgan"; // morgan import
const logger = morgan("dev"); // μ€μ μ΄ "dev"μΈ morgan λ―Έλ€μ¨μ΄λ₯Ό return ν¨
app.use(logger); // κΈλ‘λ² λ―Έλ€μ¨μ΄λ‘ μ¬μ© μ€μ 
routerλ urlμ κ·Έλ£Ήν νλ€.
μ΄λ₯Ό κΈ°μ€μΌλ‘ λλ©μΈμ μκ°ν΄λ³Έλ€.
globalRouter /μΌλ‘ μμ
videoRouter /videosλ‘ μμ
userRouter /usersλ‘ μμ
μΌλ¨μ server.js νμΌμ λ§λ€μλ€.
(μλ routeλ€μ μΌλΆλΆμΌ λΏ, μ λΆκ° μλ)
import express from "express";
// (μ€λ΅)
// λΌμ°ν° λ§λ€κΈ°
const globalRouter = express.Router();
const videoRouter = express.Router();
const userRouter = express.Router();
// λΌμ°ν° μ¬μ©νκΈ°
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);
// νμ΄μ§ λ§λ€κΈ°
globalRouter.get("/", home);
globalRouter.get("/join", join);
globalRouter.get("/login" login);
globalRouter.get("/search", search);
videoRouter.get("/:id", watch);
videoRouter.get("/id/edit", edit);
videoRotuer.get("/:id/upload", upload);
videoRotuer.get("/:id/delete", deleteVideo);
userRouter.get("/:id", edit);
userRouter.get("/:id/delete", deleteUser);
// 컨νΈλ‘€λ¬ λ§λ€κΈ°
const home = (req, res) => {
  return res.send("Home");
}
// ( ...μ΄ν μλ΅ )
routers ν΄λμ controllers ν΄λλ₯Ό λ§λ€μ΄ κ° νμΌμ λΌμ°ν°μ 컨νΈλ‘€λ¬λ₯Ό λλ μ€λ€.
λΌμ°ν°μ 컨νΈλ‘€λ¬λ₯Ό export (default) & import νλ€.
+expressλ κ° νμΌλ§λ€ import ν΄μ€μΌ νλ€.
url νλΌλ―Έν°λ₯Ό μ΄μ©ν΄ urlμ λ³μλ₯Ό μ¬μ©ν μ μλ€.
req.params: { νλΌλ―Έν° μ΄λ¦: "νλΌλ―Έν° κ°" }
req.paramsλ url νλΌλ―Έν° μ΄λ¦κ³Ό κ·Έ κ°μ λ΄μ objectμ΄λ€.
// videoRouter.js
videoRouter("/:id([0-9a-f]{24})", watch);
videoRouter.js νμΌμμ url νλΌλ―Έν° μ΄λ¦μ idλ‘ μ§μ νλ€.
req.params.idλ₯Ό μΆλ ₯νλ©΄ urlμ :id λΆλΆμ΄ μΆλ ₯λλ€.
//- video.pug
mixins video(video)
  div
    h4
      a(href=`/videos/${video.id}`)=video.title
//- home.pug
extends base
include mixins/video
block content
  each video in videos 
    +video(video)
  else
    li sorry nothing found
ννΈ, video.pug νμΌμ video mixinsμ μμ±ν μ½λμ λ°λ₯΄λ©΄,
Homeμ λμ΄λ κ° videoλ€μ titleμ ν΄λ¦νλ©΄, κ° videoμ video.idκ° ν¬ν¨λ urlλ‘ μ΄λνκ² λλ€.
μ΄λ video.pug νμΌμμ id νλΌλ―Έν°(:id) μ리μ video.idκ° μμΉν΄ μκΈ° λλ¬Έμ
id νλΌλ―Έν°(:id)λ κ³§ video.id κ° λλ€.
μ¦, req.params.id === id νλΌλ―Έν°μ κ° === video.id μ΄ λλ€.
π₯ mongoose - Schema δΈ option: id & Mongoose - λͺ¨λ₯΄λ κ² μ±μλκΈ° μ°Έκ³
κ·Έλ°λ° λλ videoSchemaμλ video documentλ₯Ό λ§λ€ λλ id νλλ₯Ό λ§λ μ μ΄ μλ€.
νμ§λ§ κ°μ§ λ°μ΄ν°λ² μ΄μ€λ‘ ν μ€νΈν λ μ¬μ©νλ μ΄ μ½λλ₯Ό κ·Έλλ‘ μ¬μ©ν΄λ μλ¬λ λ°μνμ§ μλλ€.
μ΄κ² μ΄λ»κ² λ κ±ΈκΉ?β MongoDBλ κΈ°λ³Έμ μΌλ‘ κ°κ°μ videoμ κ³ μ μλ³ λ²νΈλ‘μ¨ _idλΌλ νλμ κ°μ μΆκ°ν΄μ€λ€.
μ΄ _id νλμ κ°μ .id μ νμμΌλ‘ λΆλ¬μ μ¬μ©ν μ μλ€.
μ΄μ  urlμ λ€μ΄κ°μ λ μνλ νμ΄μ§λ₯Ό 보μ¬μ£Όλλ‘ μ»¨νΈλ‘€λ¬λ₯Ό μμ ν΄μΌ νλ€.
pugλ μ΄λ¬ν λ·°λ₯Ό λ§λλ κ²μ λλ ν
νλ¦Ώμ΄λ€.
// server.js
app.set("view engine", "pug");
// server.js
app.set("views", process.cwd() + "/src/views");
=λ³μ / #{λ³μ}
include(partials) / extends / block
쑰건문(conditionals)
λ°λ³΅λ¬Έ(iteration)
ul
  each video in videos
    li=video
λ€λ₯Έ λ°μ΄ν°λ₯Ό κ°μ§ κ°μ ννμ HTMLμ return νλ€.
includeμ ν¨κ» μ¬μ©ν΄μΌ νλ€.
watch video / edit video / upload video λ₯Ό λ€λ£Έ
pug νμΌμμ μλ°μ€ν¬λ¦½νΈ μΌν 쑰건 μ°μ°μ μ¬μ©νκΈ°
block content
  h3 #{video.views} #{video.views === 1 ? "view" : "views"}
req.body: { inputμ name: "inputμ value" }
req.bodyλ₯Ό μ΄μ©ν΄ form(method="POST")μμ μ μ‘ν κ°μ λ°μ μ μλ€.
expressκ° formμ bodyλ₯Ό μ΄ν΄ν ν μμ κ°μ μλ°μ€ν¬λ¦½νΈ objectλ‘ λ°κΏμ£Όλλ‘ νκΈ° μν΄μλ middlewareλ₯Ό μ¬μ©ν΄μΌ νλ€.
β» μ£Όμ! λͺ¨λ  middlewareλ router μ΄μ μ μμ±λμ΄μΌ νλ€.
μ΄ κ²½μ°λ₯Ό μλ‘ λ€μλ©΄, router μ΄μ μ ν΄λΉ middlewareκ° μμΉν΄μΌλ§ requestκ° videoRouterμ λμ°©νμ λ μ΄λ―Έ req.bodyκ° μ€λΉλμ΄ μμ μ μκΈ° λλ¬Έμ΄λ€.
// server.js
app.use(express.urlencoded({ extended: true }));
[TIL] 211125 μ°Έκ³
// db.js
import mongoose from "mongoose";
mongoose.connect("mongodb://127.0.0.1:27017/wetube");
Mongooseλ₯Ό λ°μ΄ν°λ² μ΄μ€(MongoDB)μ μ°κ²°μμΌ μλ²μ λ°μ΄ν°λ² μ΄μ€λ₯Ό μ°κ²°νλ€.
// server.js
import "./db";
server.js νμΌμ db.js νμΌμ import νλ€.
db.js νμΌμμ μ΄λ€ κ²λ export ν΄μ£Όμ§ μμμ§λ§, db.js νμΌμ import νλ κ²λ§μΌλ‘ μλ²λ λ°μ΄ν°λ² μ΄μ€μ μ°κ²°λλ€.
// Video.js
import mongoose from "mongoose";
const videoSchema = new mongoose.Schema({
  title: String,
  description: String,
  createdAt: Date,
  hashtags: [{ type: String }],
  mata: {
    views: Number,
    rating: Number,
  }
});
const Video = mongoose.model("Video", videoSchema);
export default Video;
model ν΄λλ₯Ό λ§λ  ν κ·Έ μμ Video.js νμΌμ λ§λ λ€.
Video.js νμΌμ videoSchemaμ Video λͺ¨λΈμ λ§λ  ν μ΄λ₯Ό export default νλ€.
Video λͺ¨λΈμ λͺ¨λκ° μ¬μ©ν  μ μλλ‘ server.js νμΌμ import νλ€.
// server.js
import "./db";
import "./models/Video";
μλ²μ λ°μ΄ν°λ² μ΄μ€κ° μ°κ²°λ ν, μ°κ²°μ μ±κ³΅νλ€λ©΄ λ°μ΄ν°λ² μ΄μ€λ Video λͺ¨λΈμ μΈμ§νκ² λλ€.
server.js νμΌμμ λΉμ₯ Video λͺ¨λΈμ μ¬μ©νμ§λ μμ κ²μ΄λ―λ‘ λ³μμ λ°μμ€μ§λ μμμ§λ§, μ΄λ κ² μμ±ν΄μ€μΌ 미리 Video λͺ¨λΈμ complie ν¨μΌλ‘μ¨ μ°λ¦¬κ° μν  λ λΆλ¬μ¬ μ μλ€.
server.js νμΌμ express λ± μλ² κ΅¬μ±κ³Ό κ΄λ ¨λ κ²λ€λ§μ λ€λ£¨κ³ ,
init.js νμΌμ λ°μ΄ν°λ² μ΄μ€μ κ΄λ ¨λ κ²λ€μ import ν ν μ΄μμ΄ μμΌλ©΄ μλ²λ₯Ό μ¬λ μν μ νλ€.
β» μ΄μ  appμ μ€ννλ νμΌμ init.js νμΌμ΄λ―λ‘ package.json νμΌμμ dev scriptλ₯Ό src/init.js λ‘ μμ ν΄μΌ νλ€.
μ΄λ₯Ό μ¬μ©νλ©΄ μλ°μ€ν¬λ¦½νΈκ° λ°μ΄ν°λ² μ΄μ€λ‘λΆν° λ°μ΄ν°λ₯Ό λ°μμ¬ λμ κ·Έ μ½λμμ λ©μΆ°μ κΈ°λ€λ €μ€λ€.
μμ±ν μμλλ‘ μ½λκ° μ€νλμ΄ μ½λ°± ν¨μλ³΄λ€ ν¨μ¬ μ½κΈ°κ° νΈνλ€.
// home 컨νΈλ‘€λ¬
export const home = async (req, res) => {
  try {
    const videos = await Video.find({});
    return res.render("home", { pageTitle: "Home", videos });
  } catch {
    return res.render("server-error");
  }
};
export const getUpload = async (req, res) => {
  return res.render("upload", { pageTitle: "Upload Video" });
};
export const postUpload = async (req, res) => {
  const { title, description, hashtags } = req.body;
  try {
    await Video.create({
      title,
      description,
      hashtags: hashtags.split(",").map(word => `#${word}`),
      createdAt: Date.now();
      meta: {
        views: 0,
        rating: 0,
      },
    });
  } catch(error) {
    return res.render("upload", { pageTitle: "Upload Video", errorMessage: error._message });
  }
  return res.redirect("/");
};
schema options
required / default / trim / maxLength / minLength
ν΄μνκ·Έ ν¬λ§·νλ λ°©λ² (μ΄κ±΄ μλ edit video ννΈμ λμ€λλ° κ·Έλ₯ μ¬κΈ°μ κ°μ΄ μμ ν¨)
μ ν 1 ~ 3 
[ Video.js ]
import mongoose from "mongoose";
// μ ν2. export formatHashtags & import in videoController.js
export const formatHashtags = (hashtags) => hashtags.split(",").map(word => word.startsWith("#") ? word : `#${word}`);
// videoSchema μμ 
const videoSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true, maxLength: 80 },
  description: { type: String, required: true, trim: true, minLength: 20 },
  hashtags: [{ type: String, required: true, trim: true }],
  createdAt: { type: Date, required: true, default: Date.now },
  meta: {
    views: { type: Number, required: true, default: 0 },
    rating: { type: Number, required: true, default: 0 },
  },
});
// μ ν1. mongoose middleware (pre hook) β νκ³: "save"μλ§ μ¬μ© κ°λ₯
videoSchema.pre("save", async function() {
  this.hashtags = this.hashtags[0].split(",").map(word => word.startsWith("#") ? word : `#${word}`);
});
// μ ν3. static λ©μλ λ§λ€κΈ°
videoSchema.static("formatHashtags", function (hashtags) {
  return hashtags.split(",").map(word => word.startsWith("#") ? word : `#${word}`);
});
const Video = mongoose.model("Video", videoSchema);
export const watch = 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("watch", { pageTitle: video.title, video });
};
export const getEdit = async (req, res) => {
  const { id } = req.params;
  const video = await Video.finById(id);
  if (!video) {
    return res.render("404", { pageTitle: "Video not found" });
  }
  return res.render("edit", { pageTitle: `Editing: ${video.title}`, video });
};
videoλ₯Ό μλ‘κ² λ§λλ κ² μλλΌ μμ νλ κ²μ΄λ―λ‘ κΈ°μ‘΄μ κ°μ΄ inputμ λ€μ΄κ° μλλ‘ value μμ±μ μΆκ°νλ€.
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 video", type="submit")
Model.exists()λ μΈμλ‘ idκ° μλλΌ νν°({ _id: id })λ₯Ό λ°λλ€
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: Video.formatHashtags(hashtags),
  });
  return res.redirect(`/videos/${id}`);
};
λ³΄ν΅ λ°μ΄ν°λ₯Ό μμ ν  λλ post requestλ μΌμ΄λμ§ μλλ€.
urlμ λ°©λ¬Ένλ©΄ λ°λ‘ videoκ° μμ λλλ‘ νλ€.
videoRouter.get("/:id([0-9a-f]{24})/delete", deleteVideo);
export const deleteVideo = async (req, res) => {
  const { id } = req.params;
  await Video.findByIdAndDelete(id);
  return res.render("/");
};
form(method="GET")μ λ§λ λ€
extends base
block content
  form(method="GET")
    input(name="keyword", placeholder="search by keyword", required, maxlength=80)
    input(value="search", type="submit")
videos λ°°μ΄μ λΉ λ°°μ΄λ‘ λ§λ λ€.
keywordκ° μ
λ ₯λμμΌλ©΄ μ΄λ₯Ό μ΄μ©ν΄ 쑰건과 μΌμΉνλ videoλ₯Ό μ°Ύμ videos λ°°μ΄μ μ
λ°μ΄νΈνλ€.
쑰건μ λ§λ videoκ° μμΌλ©΄ search νμ΄μ§μ μ΄λ₯Ό 보μ¬μ£Όκ³ , μμΌλ©΄ search νμ΄μ§λ§ 보μ¬μ€λ€.
β» search νμ΄μ§μ μ²μ μμ λ req.queryλ undefined κ°μ κ°μ§λ€. if (keyword) {}
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 });
};