Node JS #8

File Management : server.js와 init.js

프로젝트 파일이 커지면서 코드가 길어진다.

서버 관련 또한 마찬가지. server.js와 init.js로 분할하자.

server.js

  • 서버의 설정 담당

init.js

  • 서버의 초기화 담당

아래는 각 파일의 코드

//server.js (서버의 설정 담당하는 파일)

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


//configure server application
const app = express(); // application 생성
const logger = morgan("dev"); // morgan m/w 생성
app.use(logger); // m/w 등록
app.set("view engine", "pug"); // view 엔진 등록
app.set("views", process.cwd() + "/src/views"); // view 경로 등록
app.use(express.urlencoded({ extended: true })); // req.body 사용할 수 있게 m/w 등록

//router 등록
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);

export default app;
//init.js
import "./db"; // db 파일 import
import "./models/Video"; // Video model import
import app from "./server"; // app import

// 서버 가동
const PORT = 4000;
const handleListening = () =>
  console.log(
    `🚀 Server listening on port http://localhost:${PORT}`
  );

app.listen(4000, handleListening);

그리고 파일을 실행하기 위해 아래 코드를 작성하고 싶다면

> npm run dev

아래와 같이 스크립트를 변경해주자 (서버 가동 파일이 init.js로 변경되었기 때문)

//package.json
...
"scripts": {
    "dev": "nodemon --exec babel-node src/init.js"
  },
...

How to CRUD

Read

비디오 모델이 다음과 같이 정의되어 있다.

//src/movels/Video.js
import mongoose from "mongoose";

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

videoSchema.static("formatHashtags", function (hashtags) {
  return hashtags
    .split(",")
    .map((word) => (word.startsWith("#") ? word : `#${word}`));
});

const Video = mongoose.model("Video", videoSchema);
export default Video;

DB에 위와 같은 비디오들이 저장되어 있을 경우, 이를 모두 꺼내서 볼 수 있게 하자.(Read)

import Video from "../models/Video";
export const home = async (req, res) => {
  const videos = await Video.find({}).sort({ createdAt: "desc" });
  return res.render("home", {
    pageTitle: "Home",
    videos,
  });
};

위 코드를 해석하기 위해 몇가지 개념들을 살펴보자

동기(Synchronous) vs 비동기(Asynchronous)

동기(Synchronous)란 코드가 순차적으로 실행되는 것을 말한다

비동기(Asynchronous)란 코드가 병렬적으로 실행되는 것을 말한다. JS의 외부 리소스(DB)에 접근할때 자주 사용. 이때 콜백함수를 전달하여 해당 작업이 끝나고 실행해야할 것을 알려준다.

우리는 DB에 접근할 것이므로 비동기적 함수를 작성할 것인데, 비동기적 코드와 관련된 역사가 있다(콜백헬->promise->async,await)

그러나 자세한것은 나중에 알아보도록 하고, async,await가 비동기적 코드를 작성하는 가장 최신 스타일인 것 정도만 알아두고 이를 이용하자.

MDN : Async function

await는 비동기적 실행을 하고자 할때 쓰는 키워드이고, await를 쓰기 위해서는 해당 함수를 asyn 함수로 사용해야한다.

find()

DB에 저장된 데이터를 찾기 위해서는 DB에 검색해야한다. find()로 검색할 수 있다.

MongoDB : find()

The find() method has the following form:

db.collection.find( <query>, <projection>, <options> )

The find() method takes the following parameters:

Parameter Type Description
query document Optional. Specifies selection filter using query operators. To return all documents in a collection, omit this parameter or pass an empty document ({}).
projection document Optional. Specifies the fields to return in the documents that match the query filter. To return all fields in the matching documents, omit this parameter. For details, see Projection.
options document Optional. Specifies additional options for the query. These options modify query behavior and how results are returned. To see available options, see FindOptions.

코드를 다시 살펴보자

export const home = async (req, res) => { // await를 사용하기 위해 async 키워드 
  const videos = await Video.find({}).sort({ createdAt: "desc" }); // DB의 모든 Video 데이터들을 꺼내서 제작일자 기준 내림차순 정렬
  return res.render("home", {
    pageTitle: "Home",
    videos,
  });
};

만일, 비동기적 방식을 취하지 않았다면?

//sync, await 키워드 제거 -> 동기적 실행
export const home =(req, res) => { 
  const videos = Video.find({}).sort({ createdAt: "desc" });
  return res.render("home", {
    pageTitle: "Home",
    videos,
  });
};

image

다음과 같이 에러 발생

왜냐하면, DB에서 데이터를 가져오는 작업은 시간이 소요되지만 await 키워드로 인해 기다리지 않았고 따라서 빈 배열이 먼저 렌더링되어 오류가 나는것

DB에 접근할때 발생할 수 있는 에러를 해결하려면 2가지 방법이 존재

  1. try-catch
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});
  }
}
  1. callback의 error
Video.find({},(error,videos)=>{
  if(error){
    return res.render("server-error");
  }
  return res.render("home",{pageTitle:"Home",videos});
});

Role of Return

return의 일반적인 역할은 반환값을 반환하고 함수를 종료하는 것이다.

그러나 우리가 작업하는 무언가를 렌더링해줄때는 반환값이 없다.

따라서 return은 필수적이지 않다. 그러나, 함수가 확실히 종료됨을 알려주기 위하여 return을 쓰는 것이 가독성 측면에서 좋다.

// 이 코드도 정상적으로 동작
export const home = async (req, res) => {
  const videos = await Video.find({}).sort({ createdAt: "desc" });
  // return res.render()가 아님
  res.render("home", {
    pageTitle: "Home",
    videos,
  });
};

만일 return을 기재하지 않았다면 아래와 같이 실수로 render를 두번하게 되면 함수가 살아서 render를 다시하는 셈이므로 에러발생

export const home = async (req, res) => {
  const videos = await Video.find({}).sort({ createdAt: "desc" });
  res.render("home", {
    pageTitle: "Home",
    videos,
  });
  res.render("home", {});
};

따라서 그냥 속 편하게 return으로 함수를 확실하게 죽이자

export const home = async (req, res) => {
  const videos = await Video.find({}).sort({ createdAt: "desc" });
  return res.render("home", {
    pageTitle: "Home",
    videos,
  });
  res.render("home", {}); // this will be never executed. function's already dead
};

Create

Making model

데이터를 생성하기 위한 절차

  1. 데이터를 입력받기 위한 view 작성 (front-end)
  2. 데이터 생성, DB에 저장(back-end)

데이터를 입력받기 위한 view파일을 생성하자.

extends base.pug 

block content 
    if errerMessage 
        span=errorMessage
    form(method="POST")
        input(placeholder="Title",required,type="text",name="title")
        input(placeholder="Desciption",required,type="text",name="description",minlength=20)
        input(placeholder="Hashtags, separated by comma",required,type="text",name="hashtags")
        input(type="submit",value="Upload Video")

전에 살펴보았듯이, POST method form은 req.body에 input의 name들로 입력한 데이터들을 받을 수 있다.

image

console.log(req.body);

참고로 req.body를 이렇게 JSON처럼 다루기 위해서는 express.urlencoded({ extended: true })을 사전에 등록해두어야 한다.

image

이렇게 얻은 데이터를 컨트롤러에서 가공해서 새로운 video 모델을 생성

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

image

단, 지금은 video 모델을 생성만 하고 아직 DB에는 집어넣지 않은 상태이다.


Validation of model

우리는 다음과 같이 model을 생성하기 위한 schema를 정의해왔다.

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

그리고 이 schema는 데이터를 검증하는 역할을 수행한다.

예를 들어 title은 String 타입으로 정의되었으므로 5로 입력해도 “5”로 변환되며,

Number 타입인 views에 string으로 입력했을 경우에는 위 경우처럼 변환이 되지 않으므로 자동 누락된다.

mongoose가 mdel을 바탕으로 data를 검증한다.

*Date.now

Date.now()는 함수를 바로 실행한다. (X)

Date.now는 몽구스가 Model을 만들때 실행한다.(O)

*default로 기본 값을 줄 수 있다.


Saving data

데이터를 DB에 저장하는 방법은 2가지가 있다.

  1. model을 만든 후에 저장(new->save)
  2. 그냥 바로 저장(created)
export const postUpload = async (req, res) => {
  const { title, description, hashtags } = req.body;
  const video = new Video({
    title,
    description,
    hashtags: hashtags.split(",").map((word) => `#${word}`),
    createdAt: Date.now(),
    meta: {
      views: 0,
      rating: 0,
    },
  });
  await video.save(); // 데이터를 DB에 저장

  return res.redirect("/");
};

image

실제로 DB에 저장된 것을 확인할 수 있다.

여기서 _id 부분은 mongoose가 자동으로 생성한 것이다.

이 방식은 데이터를 생성한 뒤에 이를 DB에 저장한 방식이다.

create()을 활용해서 바로 저장하는 방식도 있다.

export const postUpload = async (req, res) => {
  const { title, description, hashtags } = req.body;
  try {
    await Video.create({
      title,
      description,
      hashtags: Video.formatHashtags(hashtags),
    });
    return res.redirect("/");
  } catch (error) {
    console.log(error);
    return res.render("upload", {
      pageTitle: "Upload Video",
      errorMessage: error._message,
    });
  }
};

Validation check

form의 유효성 검증은 프론트와 백에서 둘다 해주는 것이 좋다.

프론트에서는 input 태그의 속성으로 검증을 해주고 (만일 악의적 사용자가 html 수정하면 검증이 수행되지 않음)

input(name="title",required,type="text");

백에서도 검증을 다시한번 해주자.

const dataSchema=new mongoose.Schema({
	title: {type:String,required:true}; // required 속성 부여, 만일 비어있다면 DB에 생성되지 않음
})

Edit(getting data from DB)

우리가 비디어를 업로드 했다면, 이를 수정할 수도 있어야 한다.

이때 이 비디어가 가지는 고유 id를 이용해 해당 비디오에 접근할 수 있다.

그리고 이때 id를 정규식을 이용해서 추출해야 한다.

regex

image

위 mongoDB 공식문서에서 확인가능하듯이, id는 24자리의 16진수로 이루어져있다.

따라서 아래와 같이 알맞은 정규식을 적용해서 라우팅을 하자

import express from "express";
import {
  getEdit,
  postEdit,
  watch,
  getUpload,
  postUpload,
  deleteVideo,
} from "../controllers/videoController";
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("/:id([0-9a-f]{24})/delete").get(deleteVideo);

videoRouter.route("/upload").get(getUpload).post(postUpload);

export default videoRouter;

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

Hashtag issue

비디오에 해쉬태그를 다는 작업을 해준다고 하자.

video.hashtags=video.hashtags.split(,).map((word)=>`#{word}`);

그러나 이 방식은 계속해서 문자의 맨앞에 #기호를 추가하기 때문에

#이게

##이렇게 되고

###계속해서

#######추가될 수 있는 문제점이 있다.

따라서 삼항연산자를 사용하자.

video.hashtags=hashtags.split(",").map((word)=>(word.startsWith("#")?word:`#${word}));

이 코드는 startsWith를 이용해 #가 맨앞에 없다면 추가해주는 간결한 조건문이다.


Editing data

findOne()혹은 findById()로 특정 데이터를 검색할 수 있고

혹은 아래처럼 findByIdAndUpdate()함수를 통해 검색과 수정을 동시에 할 수 있다

Syntaximg

The findOneAndUpdate() method has the following form:

db.collection.findOneAndUpdate(
    <filter>,
    <update document or aggregation pipeline>, // Changed in MongoDB 4.2
    {
      writeConcern: <document>,
      projection: <document>,
      sort: <document>,
      maxTimeMS: <number>,
      upsert: <boolean>,
      returnDocument: <string>,
      returnNewDocument: <boolean>,
      collation: <document>,
      arrayFilters: [ <filterdocument1>, ... ]
    }
)
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}`);
};

Delete

삭제 기능 구현 절차

  1. form에 삭제 버튼 추가
  2. Routing
  3. Controlling

view에 삭제 버튼을 추가하자

extends base.pug

block content 
    div
        p=video.description
        small=video.createdAt
    a(href=`${video.id}/edit`) Edit Video &rarr;
    br
    a(href=`${video.id}/delete`) Delete Video &rarr;

라우팅 처리 해주자

videoRouter.route("/:id([0-9a-f]{24})/delete").get(deleteVideo);

컨트롤러 처리도

export const deleteVideo = async (req, res) => {
  const { id } = req.params;
  await Video.findByIdAndDelete(id);
  return res.redirect("/");
};

remove()와 delete()는 별차이 없지만 mongo에서는 delete() 사용을 권장한다.


Bonus : Search

비디오 검색 기능을 구현하자.

검색 기능 구현 절차

  1. 검색 view 구현
  2. Routing
  3. Countrolling

검색 view를 구현하자.

extends base.pug
include mixins/video

block content 
    form(method="GET")
        input(placeholder="Search by title",type="text",name="keyword")
        input(type="submit",value="Search now")
    each video in videos 
        +video(video)

버튼을 누르면 GET method로 인해 쿼리가 URL로 전송된다.

image

그리고 이는 req.query()로 받을 수 있다.

console.log(req.query);
// OUTPUT : { keyword: 'HELLO' }

이를 이용해 컨트롤러를 구성하자

globalRouter.get("/search", search);

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

여기서 만일 search 페이지에 처음 접근할 경우 req.query의 keyword는 undefined이기 때문에 오류방지를 위해 if 처리를 해주었다.


ETC

M/W in mongoose

mongoose에서도 미들웨어가 존재하는데, object가 DB에 저장되기 전에 작업을 해주는 것이다

mongoose document

Pre

Pre middleware functions are executed one after another, when each middleware calls next.

const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
  // do stuff
  next();
});
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}));
})

이를 통해 사전 작업을 미들웨어에서 해줄 수 있다.

그리고 이 해쉬태그 작업을 따로 함수로 만들어서 제작할 경우에는 import와 export가 필요하다

export const formatHashtags=(hashtags)=>
	hashtags.split(",").map((word)=>(word.startsWith("#")?word:`#${word}));
	
...
await Video.findByIdAndUpdate(id,{
	title, description, hashtags:formatHashtags(hashtags),
})

이때 함수를 더 쉽게 사용할 수 있는 방법이 있다.


Statics in mongoose

Statics

You can also add static functions to your model. There are three equivalent ways to add a static:

  • Add a function property to the second argument of the schema-constructor (statics)
  • Add a function property to schema.statics
  • Call the Schema#static() function
// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "statics" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
    statics: {
      findByName(name) {
        return this.find({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
  return this.find({ name: new RegExp(name, 'i') });
};
// Or, equivalently, you can call `animalSchema.static()`.
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });

const Animal = mongoose.model('Animal', animalSchema);
let animals = await Animal.findByName('fido');
animals = animals.concat(await Animal.findByBreed('Poodle'));

Do not declare statics using ES6 arrow functions (=>). Arrow functions explicitly prevent binding this, so the above examples will not work because of the value of this.

예시 코드

//static function 등록
videoSchema.static("formatHashtags",function(hashtags){
  return hashtags.split(",").map((word)=>(word.startsWith("#")?word:`#${word}`))
})

//static functino call
await Video.create({
		title,
		desciption,
		hashtags: Video.formatHashtags(hashtags),
})

Mongosh command

mongosh 사용
> mongosh

내가 가진 DB 보기
> show dbs

사용할 DB 선택
> use myDBName

DB collection 보기
> show collections

DB collection의 documents 보기
> db.collectionName.find()

DB collection의 모든 documents 제거
> db.collectionName.remove({})

How to get info from FE in BE

FE에서 BE로 정보를 보내는 방법은 크게 3가지가 있다. 해당 방식의 정보를 받는 법도 정리하자

  1. URL parameter로 보내는 경우 -> req.params
  2. form의 POST method로 보내는 경우 -> req.body (m/w 등록필요)
  3. URL GET method로 보내는 경우 -> req.query

추천 포스팅


Recap

파일을 server.js와 init.js로 분리 -> 관리 용이

CRUD의 세부적인 사항을 직접 구현

이때 DB 처럼 외부 resource에 접근할 때는 비동기적 방식사용

mongoose에 m/w로 데이터 전처리를 해도 되고, static function을 통해 전처리르 ㄹ해도 죈다.


Topic to research Later

Regulx

Syn vs Asyn in aspect of promise

Categories:

Updated:

Leave a comment