유트브 클론 코딩

히치키치·2022년 1월 20일
0

https://serendipity24.tistory.com/159?category=896597

✔ Setup

  1. git 연결
  2. npm init
  3. package.json 파일에 디버깅 부분에 스크립트 런 커맨드 추가
(...)
,
"scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
(..)

커멘드창에 npm run start 입력하면 index.js 파일 실행됨

  1. node_modules와 package.json 이해
  • node_modules : 개발에 필요한 실제 모듈이 담긴 폴더
  • package.json 파일의 dependencies : 프로젝트 실행에 필요한 모듈 목록 담김
  • 프로젝트 공유 시 실제 모듈은 공유하지 않아 .gitignore 파일에 /node_modules 라고 작성해 git에는 올라가지 않게 함
  • node_modules 폴더는 제외하고 공유하고 공유 받은 사람은 npm i를 통해 dependencies에 있는 모듈을 바로 다운 받을 수 있음
  1. babel 설치

--save-dev : devDependencies 설정

npm install @babel/core @babel/node --save-dev
npm install @babel/preset-env --save-dev

babel.config.json 파일 생성 후 작성

{"presets": ["@babel/preset-env"]}

매번 코드 작성 후 서버 재시작 하지 않고 자동으로 재시작하기 위해
nodemone 설치

npm i @babel/node nodemon --save-dev

package.json에 npm run dev 수정

"scripts": {
"dev": "nodemon --exec babel-node index.js"
},

✔ Basic server structure

1. 서버 구축

import express from "express";

const app = express();
const handleListening = () => {
  console.log("Server listening on Port 4000");
};
app.listen(4000, handleListening);

사용자의 요청에 서버는 응답해야 함

2. GET Request & Controller

const handleHome = (request,result) => console.log(req,res);
app.get("/", handleHome);

HTTP 프로토콜을 사용해 GET request를 함
1. 유저가 브라우저에 URL ("/")을 입력
2. GET request 발생
3. 서버가 받음
4. 서버가 request에 대한 response
5. express가 인자로 request를 컨트롤러인 콜백 함수에 전달함
6. 컨트롤러 콜백 함수 handleHome 실행
7. request에 대한 response로 return하는 값이 없으니 브라우저는 계속해서 로딩함

Express의 request와 respond 관련 메서드 공식 문서

3. middleware

import express from "express";

const app = express();
const PORT = 4000;

const gossipMiddleware = (req, res, next) => {
  console.log(`Someone is going to: ${req.url}`);
  next();
};
const handleHome = (req, res) => {
  return res.send("I love middlewares");
};
app.get("/", gossipMiddleware, handleHome);

const handleListening = () =>
  console.log(`✅ Server listenting on port http://localhost:${PORT} 🚀`);
app.listen(PORT, handleListening);

request와 resonse 사이에 존재함
모든 미들웨어는 핸들러이고 모든 컨트롤러는 미들웨어임
미들웨어는 (req,res,next) 인자 가짐
next()는 미들웨어가 다음 함수를 호출하게 함
미들웨어가 next()하지 않거나 next() 실행 전 return하는 경우 다음 함수는 실행되지 않음

import express from "express";

const app = express();

const PORT = 4000;

const logger = (req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
};

const privateMiddleware = (req, res, next) => {
  const url = req.url;
  if (url === "/protected") {
    return res.send("<h1>Not Allowed</h1>");
  }
  console.log("allowed");
  next();
};

const handleHome = (req, res) => {
  return res.send("I love middlewares");
};

const handleProtected = (req, res) => {
  return res.send("private area");
};

app.use(logger);
app.use(privateMiddleware);
app.get("/", handleHome);
app.get("/protected", handleProtected);

const handleListening = () =>
  console.log(`✅ Server listenting on port http://localhost:${PORT} 🚀`);

app.listen(PORT, handleListening);

app.use()는 어떤 URL에도 미들웨어로 작동하게 함
express는 위에서 아래로 실행되기 때문에 순서가 중요하다

app.get("/", handleHome);
app.use(logger);

다음과 같은 순서의 경우 handleHome에서 return이 실행되며 response가 종료되기 때무에 logger가 실행되지 않음

app.use(logger);
app.use(privateMiddleware);
app.get("/", handleHome);
app.get("/protected", handleProtected);

  1. /protected로 접속

  2. 첫번째 컨트롤러인 logger를 통해 HTTP request 종류와 라우터 URL 콘솔창 출력

  3. 두번째 컨트롤러인 privateMiddleware 실행되며 request url이 protected인지 확인

  4. protected임에 따라 respone로 Not Allowed를 반환하며 종료됨

  5. 미들웨어 handleProtected가 실행되는 걸 막음 실행되지 않음...

External Middleware

morgan 설치

npm i morgan

morgan을 호출하면 미들웨어를 반환해줌

import express from "express";
import morgan from "morgan";

const app = express();

const PORT = 4000;

const logger = morgan("dev");

const handleHome = (req, res) => {
  console.log("I'll respond");
  return res.send("Hello");
};

const handleLogin = (req, res) => {
  return res.send("Login");
};

app.use(logger);
app.get("/", handleHome);
app.get("/protected", handleLogin);

const handleListening = () =>
  console.log(`✅ Server listenting on port http://localhost:${PORT} 🚀`);

app.listen(PORT, handleListening);

"/"로 접속하고 morgan("dev") 콘솔창 결과

"dev", "common", "short", "tiny" 등 middleware 반환하는 여러 format이 있음
morgan 공식문서

✔ 라우터

1. 도메인별 라우터 나눠주기

Global Router
/
/join
/login
/search

User Router
/users/edit
/users/delete

Video Router
/videos/watch
/videos/edit
/videos/delete

videos/comments
videos/comments/delete

2. 라우터 생성

라우터 생성
각 URL에 대해 어떤 라우터 작동할지 설정

const globalRouter = express.Router();
const userRouter = express.Router();
const videoRouter = express.Router();
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);

라우터의 Controller 설정

const globalRouter = express.Router();
const handleHome = (req, res) => {
  res.send("home");
};
globalRouter.get("/", handleHome);

const userRouter = express.Router();
const handleEditUser = (req, res) => {
  res.send("Edit user");
};
userRouter.get("/edit", handleEditUser);

const videoRouter = express.Router();
const handleWatchVideo = (req, res) => {
  res.send("watch video");
};
videoRouter.get("/watch", handleWatchVideo);
  1. /videos/watch 입력
  2. /videos로 시작하는 URL에 접근하면 해당하는 videoRouterd의 컨트롤러를 찾음
  3. 해당하는 videoRouter에 /watch가 존재하고 입력한 URL과 일치함
  4. 그에 연결된 handleWatchVideo 함수 실행됨

3. Router & Controller

한 개의 파일에 서버, 라우터, 라우터 컨트롤러까지 설정하니 코드가 더럽다!
라우터 폴더와 컨트롤러 폴더에 각각 라우터와 컨트롤러를 작성하고 이를 import/export해서 쓰자

폴더 구조

export/import 규칙

다음과 같이 export default {모듈명}로 한개의 파일에서 한개의 모듈만 export 함

/routers/videoRouter.js

import express from "express";
import { see, edit, deleteVideo, upload } from "../controllers/videoController";

const videoRouter = express.Router();

videoRouter.get("/:id", see);
videoRouter.get("/:id/edit", edit);
videoRouter.get("/:id/delete", deleteVideo);
videoRouter.get("/upload", upload);

export default videoRouter; 

이렇게 export default한 것을 import해서 다른 파일에서 사용하는 경우 export한 모듈명과 꼭 동일하게 써줘야할 필요는 없다!! (애초에 그 파일에서 내보낸 것도 그거 하나니까 받아올 것도 그거 하나 뿐이라 그런듯!!)

server.js

import videoRouter from "./routers/videoRouter";

다음과 같이 여러개를 export 할 때는 export {모듈명}

/controllers/videoController.js

export const trending = (req, res) => res.send("Home");
export const see = (req, res) => res.send("Watch video");
export const edit = (req, res) => res.send("Edit");
export const search = (req, res) => res.sned("Search");
export const deleteVideo = (req, res) => res.send("delete video");
export const upload = (req, res) => res.send("upload video");

이렇게 여러개의 모듈을 export 하는 파일에서 일부를 import 해서 써야하는 경우
import {export 파일과 동일한 모듈명1, 모듈명2,...} from "파일경로"
로 가져와 작성해야함 꼭 동일한 모듈명이어야함

routets/videoRouter.js

import { see, edit, deleteVideo, upload } from "../controllers/videoController";

4. URL Parameters

작동 방식

routers/videoRouter.js

(..)
videoRouter.get("/upload", upload);
videoRouter.get("/:id", see);
videoRouter.get("/:id/edit", edit);
videoRouter.get("/:id/delete", deleteVideo);

/video/1234/edit 요청 시 라우터의 /:id/edit의 경우로 인식함
/ 다음에 :id 붙여 해당 자리에 할당되는 값은 id 파라미터라고 명명함

순서 주의!!

routers/videoRouter.js

(..)
videoRouter.get("/upload", upload);
videoRouter.get("/:id", see);

이 경우 /video/upload 요청시 upload 컨트롤러가 실행됨

(..)
videoRouter.get("/:id", see);
videoRouter.get("/upload", upload);

그러나 위와 같이 순서가 바뀐 경우 video/upload 요청 시 js는 top to bottom 방식으로 작동하여 /upload/upload가 아닌 먼저 등장한 /:id로 인식해서 see 컨트롤러가 실행됨

형식 주의

routers/videoRouter.js

videoRouter.get("/upload", upload);
videoRouter.get("/:id", see);
videoRouter.get("/:id/edit", edit);
videoRouter.get("/:id/delete", deleteVideo);

/video/안녕하세요/delete 요청시 id 파라미터로 안녕하세요가 할당됨
but!! 우리는 video id로 해당 값은 반드시 숫자이다. 숫자가 아닌 다른 형식의 데이터가 파라미터로 전달되어 컨트롤러가 실행되는 오류 발생하지 말아야 함
=> 정규 표현식을 사용해 조건 부여

videoRouter.get("/upload", upload);
videoRouter.get("/:id(\\d+)", see);
videoRouter.get("/:id(\\d+)/edit", edit);
videoRouter.get("/:id(\\d+)/delete", deleteVideo);

숫자만 선택하는 \d+ 이용함 (js 문법에 따라 \를 한번더 써줌)
정규표현식 테스트 사이트 이용해 다양한 조건 부여 가능

https://andybrewer.github.io/mvp/

✔ mongoDB

JSON-like-document

1. 연결 설정

  1. 몽고디비가 설치된 경로로 이동
cd C:\Program Files\MongoDB\Server\5.0\bin
  1. connecting to: ?? 확인
mongo
  1. DB 연결 확인
show dbs
  1. 서버 레포에 몽구스 설치
npm i mongoose
  1. 몽구스와 몽고디비 연결 확인

    connection to : 전체 사용이 아니라 mongodb://127.0.0.1:27017/{프로젝트명} 으로 MONGO_URL 값을 가져옴

  2. server.js와 같은 위치에 db.js 작성

import mongoose from "mongoose";

mongoose.connect("mongodb://127.0.0.1:27017/Youtube", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const db = mongoose.connection;
const handleOpen = () => console.log("✔ Connection to DB");
const handleError = (error) => console.log("❌ DB Error", error);

db.on("error", handleError);
//on - 여러번 실행 가능
//DB 연결에서 에러가 발생할 때마다

db.once("open", handleOpen);
//once - 한번만 실행 가능
//DB 연결이 된 경우
  1. server.js에 db.js 파일을 import하기
(...)
import userRouter from "./routers/userRouter";
import videoRouter from "./routers/videoRouter";
import "./db"; 
(...)

2. Video 모델 생성

1. 모델 디렉토리 생성 + Video 모델 작성 파일 생성

2. Video 모델 스키마 작성

mongoose.Schema 사용해 스키마 작성 = 모델의 형태 정의

//스키마 예시
 const {모델명}=new mongoose.Schema({
   속성1: 타입,
   속성2: String,
   속성3 : Data,
   속성4: [{ type: String }],
   속성5: {
      속성1: Number,
      속성2: Number,
    },
});

⭐ 구체적인 값을 부여하는 것이 아닌, 해당 속성에 따른 타입을 지정함

3. 모델 생성

mongoose.model 에서 모델 이름과 스키마를 인자로 모델 생성

const {모델명} = mongoose.model("{이름}", {스키마});

4. 다른 스크립트에서도 사용 가능하게 export

주로 models 폴더 안에 모델 1개 당 js 스크립트 한 개 작성하여 export default 사용

//export 가능하게...
export default Video;

5. 다른 파일에서 사용하기 위해 import

//server.js
(...)
 import "./db";
import "./models/Video";
//모두가 Video 모델 알도록 import 해오기

(...)

3. Query

1. 파일 분리

server.js : express와 server의 configuration 관련된 코드 처리
init.js : database와 models을 import하기 위함

//server.js
import express from "express";
import morgan from "morgan";

import globalRouter from "./routers/globalRouter";
import userRouter from "./routers/userRouter";
import videoRouter from "./routers/videoRouter";

//console.log(process.cwd());

const app = express();

const logger = morgan("dev");

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");

app.use(logger);
app.use(express.urlencoded({ extended: true }));
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);

export default app;

//init.js
import "./db";
import "./models/Video";
import app from "./server";

const PORT = 4000;
const handleListening = () =>
  console.log(`✅ Server listenting on port http://localhost:${PORT} 🚀`);

app.listen(PORT, handleListening);

npm run dev 명령러로 init.js 파일 실행되도록 package.json 수정

(...)
   "scripts": {
    "dev": "nodemon --exec npx babel-node src/init.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
(...)

2-1. Query & callbackFunction

쿼리 공식 문서

videoController.js

Video 모델의 데이터 전체({})를 불러오고 나서 콜백 함수가 실행됨

import Video from "../models/Video";

export const home = (req, res) => {
  console.log("start home");
  Video.find({}, (error, videos) => {
    console.log("Finish Search");
    console.log("error", error);
    console.log("videos", videos);
  });
  console.log("wait for rendering");
  res.render("home", { pageTitle: "Home", videos: [] });
};

2-2. Query & await / async

공식 문서
1. async function 선언은 AsyncFunction객체를 반환하는 하나의 비동기 함수 정의
2. 비동기 함수는 이벤트 루프 통해 비동기적으로 작동하는 함수로, 암시적으로 Promise 사용해 결과 (성공/실패) 반환
3. try-catch 문을 사용해 성공 실패 여부에 따라 무엇을 수행할지 지정

import Video from "../models/Video";

export const home = async (req, res) => {
  try {
    const videos = await Video.find({});
    return res.render("home", { pageTitle: "Home", videos });
  } catch (error) {
    return res.render("server-error", { error });
  }
};

3. Return & Render

return으로 function을 마무리하고 있음
render 한 번하고 다시 render 하면 express에서 오류 발생

4. create collections
req.body에서 video DB 생성에 필요한 title, description, hashtags를 가져옴

const { title, description, hashtags } = req.body;
//console.log(title, description, hashtags);

video 모델 생성 후 가져온 값들을 모델에 넣어줌

  const video = new Video({
    title,
    description,
    createdAt: Date.now(),
    hashtags: hashtags.split(",").map((word) => `#${word}`),
    meta: {
      views: 0,
      rating: 0,
    },
  });
  //console.log(video);

생성한 video 모델을 mongoDB에 넣어줌

video.save()

로 간단하지만 video 모델이 완전히 저장되기 전까지 함수를 마무리하는 return이 실행되면 안된다!! 따라서 await/async 를 사용해 비동기적으로 진행함

export const postUpload = async (req, res) => {
  (...)
   const dbVideo = await video.save();
  //console.log(dbVideo);
  return res.redirect("/"); //홈으로 redirect
};

다음과 같은 비동기적 코드를 통해 video가 mongoDB에 다 저장된 이후에 홈으로 리다이렉트 (돌아가는) return문이 실행됨

export const postUpload = async (req, res) => {
  const { title, description, hashtags } = req.body;
  //console.log(title, description, hashtags);
  const video = new Video({
    title,
    description,
    createdAt: Date.now(),
    hashtags: hashtags.split(",").map((word) => `#${word}`),
    meta: {
      views: 0,
      rating: 0,
    },
  });
  //console.log(video);
  const dbVideo = await video.save();
  //console.log(dbVideo);
  return res.redirect("/");
};

5. MongoDB 명령어로 collection 저장 여부 확인

//MongoDB가 설치된 경로로 이동
cd ~~~

//몽고 디비 실행
mongo

//로그인된 계정의 전체 디비 
show dbs

//살펴볼 DB 선택
use {db명}

//DB에 있는 collections 목록 보기
show collections

//collections의 data 살펴보기
db.{collections 이름}.find()

6. 모델 생성 과정 중 Validation

모델 생성 시, 입력값 타입과 입력 여부에 따라 모델 생성에서 에러 발생 가능

try-catch 이용해 처리

  try {
    await Video.create({
      title,
      description,
      createdAt: Date.now(),
      hashtags: hashtags.split(",").map((word) => `#${word}`),
      meta: {
        views: 0,
        rating: 0,
      },
    });
    return res.redirect("/");
  }
  catch (error){
    console.log(error);
    return res.render("upload",{pageTitle: "Upload Video", errorMessage : error._message})
  }

에러 메시지가 보이도록 함

extends base.pug

block content 
    if errorMessage  //에러 있는 경우
        span=errorMessage //에러 보여주기
    form(method="POST", action="/videos/upload")
        input(placeholder="Title",name="title" type="text" required)
        input(placeholder="Description",name="description" type="text" required)
        input(placeholder="Hashtags",name="hashtags" type="text" required)
        input(type="submit", value="Upload Video")

DB에 collection을 생성할 때 꼭 필수 입력 (required : true)과 따로 입력되는 값이 없는 경우 기본값 설정 (required : true)해주도록 작성

const videoSchema = new mongoose.Schema({
  title: { type: String, required: true},
  description: { type: String, required: true},
  createdAt: { type: Date, required: true, default: Date.now },
  hashtags: [{ type: String, trim: true }],
  meta: {
    views: { type: Number, required: true, default: 0 },
    rating: { type: Number, required: true, default: 0 },
  },
});

createdAt 속성은 입력 필수값으로 required: true이고 별로의 입력이 주어지지 않은 경우 현재 날짜로 값을 주기 위해 default: Date.now 로 작성함
default: Date.now() 가 되면 바로 실행되는 것임으로 모델이 생성될 때 Date 함수가 실행되도록 default: Date.now으로 작성함

 createdAt: { type: Date, required: true , default: Date.now},

⭐ 몽고디비 스키마 타입 확인 & 정의
https://mongoosejs.com/docs/schematypes.html
https://mongoosejs.com/docs/guide.html

입력 시 조건 (글자수 제한 등등) client와 서버 동시에 둘 다 구현되어야 함

client 부분

block content 
    if errorMessage 
        span=errorMessage
    form(method="POST", action="/videos/upload")
        input(placeholder="Title",name="title" type="text" required, maxlength=80)
        input(placeholder="Description",name="description" type="text" required, minlength=20)
        input(placeholder="Hashtags",name="hashtags" type="text" required, name="hashtags")
        input(type="submit", value="Upload Video")

server 부분

//model의 scheme 정하기 (video의 형태 정의)
//구체적인 value 부여 X, 속성에 따른 타입 선언
const videoSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true, maxLength: 80 },
  description: { type: String, required: true, trim: true, minLength: 80 },
  createdAt: { type: Date, required: true, default: Date.now },
  hashtags: [{ type: String, trim: true }],
  meta: {
    views: { type: Number, required: true, default: 0 },
    rating: { type: Number, required: true, default: 0 },
  },
});

정규 표현식과 id 연결
정규식 연습할 수 있는 사이트
정규식에 대한 MDN의 공식 문서

몽고 DB에서 자동으로 부여하는 id가 _id: new ObjectId("61ff42ea8765cdbf2aef90e5") 이런 식으로 숫자와 영문이 섞여 있기 때문에 video 라우터에서 id에 대한 파라미터 정규식을 다음과 같이 수정함

const videoRouter = express.Router();

videoRouter.get("/:id([0-9a-f]{24})", watch);
videoRouter.route("/:id([0-9a-f]{24})/edit").get(getEdit).post(postEdit);
videoRouter.route("/upload").get(getUpload).post(postUpload);

video를 불러오지 못했을 때 ERROR VIEW 만들기

  1. 404 pug 작성
extends base
  1. 해당 id의 video가 존재여부에 따라 페이지 render함
  • if 조건문으로 video가 null 인 경우
    404 Error view render -> return 함수 종료
  • video 존재로 null이 아닌 경우
    if문 false임
    상세정보(watch) view render -> return 함수 종료
export const getEdit = async (req, res) => {
  const { id } = req.params;
  const video = await Video.findById(id);
  console.log(video); //해당 id값의 video async-await로 받아오기
  if (video === null) { //id에 해당하는 video 존재 X인 경우
     return res.render("404", { pageTitle: "Video not found" });
    //404 pug 랜더 -> 함수 종료
  }
  return res.render("watch", { pageTitle: video.title, video });
  //watch pug 랜더 -> 함수 종료
}

해당 id의 존재 여부 확인 후 속성 값 Update

  1. .findByIdfilter를 사용해 존재 여부 확인
  • .findById 사용 시 해당 id를 가지는 모델 collection 값 전체 가져옴
  • if문에는 전체가 필요한 것이 아니라 존재의 유무가 필요해 .exists(); 사용
  • .exists()의 filter로 _id:id로 설정해 video 모델의 collection 중 _id 값이id인 경우 true 반환함
const video = await Video.exists({ _id: id });
  1. findByIdAndUpdate 이용해 입력값 UPDATE
  • await/async 사용
  • 해시태그 입력값에 이미 # 존재하는 경우 #를 추가하지 않음
  • UPDATE가 다 된 경우, UPDATE한 collection 상세 정보를 보여주는 페이지로 이동
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 === null) {
    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}`);
};

middleware

Mongoose Middleware
document middleware, model middleware, aggregate middleware, query middleware 존재
https://mongoosejs.com/docs/middleware.html#middleware

document middleware 함수에서 this는 현재 document 참조
https://mongoosejs.com/docs/middleware.html#types-of-middleware

//middleware 설정
videoSchema.pre("save", async function () {
  //console.log("We are about to save : ", this);
  this.hashtags = this.hashtags[0]
    .split(",")
    .map((word) => (word.startsWith("#") ? word : `#${word}`));
});

0개의 댓글