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