[Node] #6 MONGODB AND MONGOOSE

[Node] #6 MONGODB AND MONGOOSE

#6.0 ~ 6.1 Array Database

Absolute URL and Relative URL

href의 앞머리에 / 를 넣으면 어디있든 상관없이 root경로 + /

/를 지우면 현재경로에 / 붙혀서 감

//=> 제일앞에 /가있으면 절대경로(videos router에서 실행) a(href="/video/edit")--->localhost:4000/video/edit a(href="video/edit")--->localhost:4000/videos/video/edit a(href=`${video.id}/edit`)--->localhost:4000/videos/1/edit

#6.2 ~ 6.3 Edit Video

**method는 form과 back end 사이의 정보 전송에 관한 방식**

get

그 form에 있는 정보가 url에 들어가 검색할 때 필요 검색페이지를 만들 때 사용 그 검색어는 url안에 들어감, 데이터를 오직 받는 목적이라면 get

post

post 방식은 파일을 보내거나, database에 있는 값을 바꾸는 뭔가를 보낼 때 사용함

웹사이트에 로그인 할때도 사용

route()를 이용한 shortcut

하나의 URL에 get,post 방식을 쓰도록 할때 유용함.

videoRouter.route("/:id(\\d+)/edit").get(getEdit).post(postEdit); /* videoRouter.get("/:id(\\d+)/edit", getEdit); videoRouter.post("/:id(\\d+)/edit", postEdit); 와 같음 */

res.redirect()

브라우저가 해당 URL redirect (자동으로 이동)하도록 하는 것.

app.use(express.urlencoded({extended: true}));

express application가 form의 value들을 이해할수 있도록 하고 우리가 쓸 수 있는 자바스크립트 형식으로 변형시켜 준다

req.body: form의 value값 받기 ex) const {username, password} = req.body

input에 name넣는걸 까먹으면 req.body에는 데이터가 없다

#6.4 Recap

req.body

form 안에 있는 value의 javascript representation app.use(express.urlencoded)를 이용해야만 사용가능. 해당 middleware의 순서도 중요! videoRouter보다 먼저 앞에 와야만 req.body가 준비되있음. input에 name을 설정해주지 않으면, req.body는 비어있음.

mongodb and mongoose

mongodb 초급자,상급자 모두 다루기 좋은 database. mongoose 이를 통해 자바스크립트에서 mongodb와 상호작용 할 것.

#6.7 Introduction to MongoDB

MongoDB

general purpose document-based 큰 장점.

보통은 sql-based (액셀처럼 행과열이있는 것)임. database를 object로 생각 JSON-like documents.

document를 검색하고 만들거나 삭제할 기회를 줌.

#6.8 Connecting to Mongo

Windows 에서 mongo 설치했는데 cmd에서 mongodb커맨드 안될 때

https://dangphongvanthanh.wordpress.com/2017/06/12/add-mongos-bin-folder-to-the-path-environment-variable/

mongoose

node.js와 mongoDB를 이어주는 다리 validation, query building, hook 등 많은게 존재

터미널로 mongoDB 확인

mongod입력으로 설치 확인 mongo 입력으로 mongodb실행 help 사용가능한 명령어 볼 수 있음

show dbs 가지고 있는 DB 보여줌

mongoose 패키지설치

npm i mongoose

mongoose 연결

//db.js import mongoose from "mongoose"; //terminal에서 mongo 실행뒤, 나오는 URL복사. // 뒤에는 꼭 이름(wetube)을 추가해줘야함. mongoose.connect("mongodb://127.0.0.1:27017/wetube");

//server.js import "./db"; // 파일 그 자체를 import함. 이 파일을 import 해줌으로써, 서버가 mongo에 연결됨.

db.js 설정

//db.js import mongoose from "mongoose"; mongoose.connect("mongodb://127.0.0.1:27017/wetube", { useNewUrlParser: true, useUnifiedTopology: true, }); const db = mongoose.connection; const handleOpen = () => console.log("Connected to DB ✓"); const handleError = (error) => console.log("DB Error", error); //on은 여러번 발생 시킬 수 있음 like 클릭이벤트 db.on("error", handleError); //once는 한번만 발생 db.once("open", handleOpen);

#6.9 CRUD Introduction

C: Create 유저생성, 비디오업로드, 댓글생성 등

R: Read 비디오, 검색 등

U: Update 유저정보 수정, 비디오 수정 등

D: Delete 회원탈퇴, 댓글삭제 등

#6.10 VIdeo Model

데이터 타입 정하기

어떤 유형의 Data를 받는지 정해주기.

실제적인 내용을 넣는건 user의 몫

//Video.js import mongoose from "mongoose"; const videoSchema = new mongoose.Schema({ title: String, description: String, createdAt: Date, hashtags: [{ type: String }], // meta는 user가 입력할 필요없음. meta: { views: Number, rating: Number, }, }); const Video = mongoose.model("Video", videoSchema); export default Video; //server.js //server.js에 database를 import해서 연결 시킨후, //해당 연결이 성공적일 때, video를 import해줌. //db를 mongoose와 연결시켜서 video model을 인식시킴. import "./db"; //db.js import "./models/Video";

#6.11 Our First Query

Server.js와 init.js

관련된 것들끼리 역할을 분리시켜줌. sever server의 구성에 관련된 코드만 담기 init 필요한 모든 것들을 import 시키는 역할 import에 문제가 없다면 app을 실행

package.json에서 시작 파일을 init.js로 지정.

//server.js import express from "express"; import morgan from "morgan"; import globalRouter from "./routers/globalRouter"; import videoRouter from "./routers/videoRouter"; import userRouter from "./routers/userRouter"; 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 listening on port http://localhost:${PORT} ✓`); app.listen(PORT, handleListening);

Videomodel 사용

callback 무언가가 발생하고 난 다음 호출되는 function database는 자바스크립트의 밖에 존재하기 때문에, 약간의 기다림이 필요하다. 기다림을 표현하는 방식 app.listen(PORT, handleListening) 연결이 확인되면, handleListening 발동

Video.find({})

{}는 serch terms, 이게 비어있으면 모든 형식을 찾는다

logger은 request가 완성되면 출력이 돼야함

//videoController.js import Video from "../models/Video"; export const home = (req, res) => { // find (serch terms, callback(err, documents)) // {} : search terms가 비어있으면 모든형식을 찾는다는 의미 Video.find({}, (error, videos) => {}); return res.render("home", { pageTitle: "Home" }); };

#6.12 Our First Query part Two

callback

Video.find({}, (error, videos) => {});

mongoose는 {}부분을 database에서 불러온다. database가 반응하면 mongoose는 (error, videos) function을 실행시킨다.

출력순서

코드에 따라 실행되는 시간이 다를 수 있다.

바깥 console.log과 GET request가 끝난 후, video 내부 console.log가 실행되는 모습.

export const home = (req, res) => { Video.find({}, (error, videos) => { console.log("errors", error); console.log("videos", videos); }); console.log("hello"); return res.render("home", { pageTitle: "Home", videos: [] }); };

#6.13 Async Await

callback

장점 에러들을 여기에서 바로 볼 수 있음.

//videoController.js // callback 방식 Video.find({}, (error, videos) => { if (error) { return res.render("server-error"); } return res.render("home", { pageTitle: "Home", videos }); });

promise

callback의 최신 버전

async안의 function일때만, await 사용가능

//videoController.js export const home = async (req, res) => { /* await가 있으면, find는 callback을 필요로 하지 않는다는 것을 알게됨. 찾아낸 Video를 바로 출력해줌. await는 database를 기다려줌.즉 위의 예시처럼 뒤죽박죽 콘솔로그 되지 않고, 직관적으로 top to bottom으로 실행됨*/ const videos = await Video.find({}); return res.render("home", { pageTitle: "Home", videos }); };

#6.14 Returns and Renders

return의 역할

본질적인 return의 역할보다는 function을 마무리짓는 역할로 사용되고 있음.

return이 없어도 정상적으로 동작하지만 실수를 방지하기 위해 return을 사용render한 것은 다시 render할 수 없음

redirect(), sendStatus(), end() 등등 포함 (express에서 오류 발생)

#6.15 Creating a Video part One

Video 생성

정보를 받을 input 생성

// upload.pug extends base.pug block content form(method="POST") // name이 있어야만, req.body 정보를 받아올 수 있음. input(name="title", placeholder="Title", required, type="text") input(name="description", placeholder="Description", required, type="text") input(name="hashtags", placeholder="Hashtags, separated by comma.", required, type="text") input(type="submit", value="Upload Video")

Schema 형태 정해주기

//Video.js import mongoose from "mongoose"; const videoSchema = new mongoose.Schema({ title: String, description: String, createdAt: Date, hashtags: [{ type: String }], meta: { views: Number, rating: Number, }, }); const Video = mongoose.model("Video", videoSchema); export default Video;

POST 작업시, 작동하는 function 만들기

//videoController.js import Video from "../models/Video"; export const postUpload = (req, res) => { const { title, description, hashtags } = req.body; const video = new Video({ title, description, createdAt: Date.now(), //split : , 단위로 스트링을 array로 나누어줌. //map : 앞에 #를 붙여줌 hashtags: hashtags.split(",").map((word) => `#${word}`), meta: { views: 0, rating: 0, }, }); console.log(video); return res.redirect("/"); };

#6.16 Creating a Video part Two

mongoose가 어느정도 data validation을 하고 있다. 정해준 data type과 다른 정보를 넣으면 출력하지 않음.

Database에 정보 저장하기

//videoController.js import Video from "../models/Video"; export const home = async (req, res) => { const videos = await Video.find({}); return res.render("home", { pageTitle: "Home", videos }); }; // 방법1. await.video.save() export const postUpload = async (req, res) => { const { title, description, hashtags } = req.body; const video = new Video({ title, description, createdAt: Date.now(), hashtags: hashtags.split(",").map((word) => `#${word}`), meta: { views: 0, rating: 0, }, }); //database에 저장. await를 함으로써 해당코드가 완료될때까지 기다림. await video.save(); return res.redirect("/"); //방법2. await Video.create() export const postUpload = async (req, res) => { const { title, description, hashtags } = req.body; await Video.create({ title, description, createdAt: Date.now(), hashtags: hashtags.split(",").map((word) => `#${word}`), meta: { views: 0, rating: 0, }, }); return res.redirect("/"); }; };

터미널에서 확인이 가능해짐.

#6.17 Exceptions and Validation

Error가 발생하는 경우 잘못된 data type 입력 number를 요구하는데 string을 입력한 경우 required를 요구한 datatype을 입력하지 않은 경우. !

try, catch를 이용해 error시 기능 구현

//videoController.js export const postUpload = async (req, res) => { const { title, description, hashtags } = req.body; try { await Video.create({ title, description, hashtags: hashtags.split(",").map((word) => `#${word}`), }); return res.redirect("/"); } catch (error) { return res.render("upload", { pageTitle: "Upload Video", errorMessage: error._message, }); } };

Schema에서 Default값을 주기

//Video.js import mongoose from "mongoose"; const videoSchema = new mongoose.Schema({ title: { type: String, required: true }, description: { type: String, required: true }, // Date.now()가 아닌 Date.now 를 한 이유: 함수가 바로 실행되는 걸 원하지 않아서. createdAt: { type: Date, required: true, default: Date.now }, hashtags: [{ type: String }], meta: { views: { type: Number, default: 0, required: true }, rating: { type: Number, default: 0, required: true }, }, }); const Video = mongoose.model("Video", videoSchema); export default Video;

#6.18 More Schema

[Mongoose v5.13.2: SchemaTypes](https://mongoosejs.com/docs/schematypes.html)

데이터에 대한 구체적인 설정은 매우 중요함

minlength, maxlength input과 schema 둘다 설정해두는게 보안에 뛰어남.

SchemaTypes 예시 trim string 양 옆의 빈공간을 없애줌 ex) “ h . “””” ⇒ “h .”

trim, minLength 설정

//Video.js 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, required: true }, rating: { type: Number, default: 0, required: true }, }, });

minLength 설정

//upload.pug extends base.pug block content if errorMessage span=errorMessage form(method="POST") input(name="title", placeholder="Title", required, type="text", maxlength=80) input(name="description", placeholder="Description", required, type="text", minlength=20) input(name="hashtags", placeholder="Hashtags, separated by comma.", required, type="text") input(type="submit", value="Upload Video")

video mixin내용 database 내용 표기하도록 변경

//video.pug mixin video(video) div h4 a(href=`/videos/${video.id}`)=video.title p=video.description small=video.createdAt hr

#6.19 Video Detail

오류1 : upload href 접속시 오류

videoRouter.get("/:id(\\d+)", watch);

현재 mongodb의 id는 문자가 들어간 불규칙한 스트링으로 위의 정규표현식과는 모순되서 오류가 발생한다.

mongodb의 id는 24글자의 16진수

[ ObjectIds in Mongoose ]( https://masteringjs.io/tutorials/mongoose/objectid )

오류2 : 변수 video가 undefined 상태라 불러올수 없음

//watch.pug extends base.pug block content div p=video.description small=video.createdAt a(href=`${video.id}/edit`) Edit Video →

해결1 : URL에 정규표현식, regexpal.com

[0-9a-f] : 0-9의 숫자 혹은 a-f까지의 문자를 사용하는지 확인(16진수)

{24} : 24글자로 이루어져있는가?

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

해결2 : findById로 데이타베이스에서 video object 불러오기

id는 받을 수 있으므로, id를 이용해서 찾기 findById 위의 URL 변환 과정을 거쳤으므로, req.params로 URL에서 id를 가져올 수 있는 것.

//videoController.js export const watch = async (req, res) => { //해당 URL의 ID를 가져온다. const { id } = req.params; //ID와 일치하는 video object를 database에서 가져온다. const video = await Video.findById(id); //video object를 watch.pug로 보낸다. return res.render("watch", { pageTitle: video.title, video }); };

#6.20 Edit Video part One

exec()

execute를 호출하면 promise가 return 될 것 이다.

현재 지워도 상관없는 이유는, async랑 await를 쓰고 있기 때문.

//videoController.js export const watch = async (req, res) => { const { id } = req.params; const video = await Video.findById(id).exec(); return res.render("watch", { pageTitle: video.title, video }); };

error 1. (URL에 임의로 id 작성후 접속시)

만약 URL에 다른 id를 적어서 접속한다면? ex)localhose4000:/videos/sadasdsa231321asf

null로부터 title이라는 property를 찾을 수 없음. video 검색에 실패했으므로 null

프로그래머는, 계획 외에 실패한 경우도 고려해서 설계해야함

해결 1. if를 통해 video가 있는지 확인하고 렌더링

//videoController.js export const watch = async (req, res) => { const { id } = req.params; const video = await Video.findById(id); //new if (video) { return res.render("watch", { pageTitle: video.title, video }); } return res.render("404", { pageTitle: "Video not found." }); }; //404.pug extends base.pug

Edit video

//videoController.js export const getEdit = async (req, res) => { const { id } = req.params; const video = await Video.findById(id); // vide===null 인 경우 if (!video) { return res.render("404", { pageTitle: "Video not found." }); } return res.render("edit", { pageTitle: `Edit: ${video.title}`, video }); }; //edit.pug extends base.pug block content h4 Change Title of Video form(method="POST") input(name="title", placeholder="Video Title", value=video.title, required) input(name="description", placeholder="Description", required, type="text", minlength=20, value=video.description) input(name="hashtags", placeholder="Hashtags, separated by comma.", required, type="text", value=video.hashtags) input(value="Save", type="submit")

problem 1 : hashtag가 array형태 그대로 출력됨.

해결 1 : join()을 이용해 format 변경

input( (name = "hashtags"), (placeholder = "Hashtags, separated by comma."), required, (type = "text"), (value = video.hashtags.join()) );

#6.21 Edit Video part Two

POST Request

방법 1 직접 값을 변경하고 마지막에 await video.save()로 저장

//videoController.js export const postEdit = async (req, res) => { const { id } = req.params; const { title, description, hashtags } = req.body; const video = await Video.findById(id); if (!video) { return res.render("404", { pageTitle: "Video not found." }); } video.title = title; video.description = description; video.hashtags = hashtags.split(",").map((word) => `#${word}`); await video.save(); return res.redirect(`/videos/${id}`); };

문제 1 : hashtag #이 무분별하게 붙음

단어앞에 무조건 #이 추가되기에, edit할때마다 계속 붙어버림

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

해결 1 : word.startsWith()

’#’이 붙으면 word 그대로 출력하고, 없다면 추가함.

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

#6.22 Edit Video part Three

export const watch = async (req, res) => { const { id } = req.params; //const video = await Video.exists({_id:id}); const video = await Video.findById(id); if(video===null){ return res.render("404", { pageTitle: "Video not found" }); } return res.render("watch", {pageTitle: video.title, video}) };

여기에서는 exist를 사용하면 안됨, object를 edit templete으로 보내줘야함

exist는 그냥 존재여부 확인

#6.23 Middlewares

몽고 사용하기

mongo

내가 가진 db 보기

show dbs

현재 사용 중인 db 확인

db

사용할 db 선택하기

use dbName (현재 수업에서는 ` use wetube ` )

db 컬렉션 보기

show collections

db 컬렉션 안에 documents 보기

db.collectionName.find() (현재 수업에서는 ` db.videos.find() ` )

db 컬렉션 안에 documents 모두 제거하기

db.collectionName.remove({}) (현재 수업에서는 ` db.videos.remove({}) ` )

**[몽구스 가이드]**

Express에서의 middleware

request를 중간에 가로채서 뭔가를 하고 이어서 진행하는 것

뭔가를 하고, next를 콜 한 다음 request를 계속 처리

mongoose에서의 middleware

document에 무슨 일이 생기기 전이나 후에 middleware를 적용할 수 있음

middleware는 무조건 model 이 생성되기 전에 만들어야 함

this : 우리가 저장하고자 하는 문서

middleware

** model이 생성되기 전에 만들어야 함 **

ex)

//middleware userSchema.pre("save", async function(){ this.password = await bcrypt.hash(this.password,5); }) //model const User = mongoose.model("user", userSchema);

**default export는**

import Video, { formatHashtags } from "../models/Video" 라고 치면 Video가 defalut export고 {formatHashtags}가 export

#6.24 Statics

save hooks 같은 경우는 update할 문서에 접근이 가능함

findOneAndUpdate의 경우에는 접근할 수 없음

hashtags array 및 # 문제를 깔끔하게 해결하기

findByIdAndUpdate()에서는 save 훅업이 발생하지 않음 => 다른 방법을 알아보자 Video.js에 function을 만들어서 관리하기 => 이것도 괜찮음 근데 다른것도 알아보자 static을 사용하면 import 없이도 Model.function()형태로 사용이 가능함 => super cool

방법 1. 함수를 만들고 export해서 처리

//Video.js export const formatHashtags = (hashtags) => hashtags.split(",").map((word) => (word.startsWith("#") ? word : `#${word}`)); //videoController.js 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: formatHashtags(hashtags), }); return res.redirect(`/videos/${id}`); };

방법 2. static

Video.Create, Video.findById와 같이 우리가 직접 만들 수 있음.

//Video.js videoSchema.static("formatHashtags", function (hashtags) { return hashtags .split(",") .map((word) => (word.startsWith("#") ? word : `#${word}`)); }); //videoController.js 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}`); };

글에서 hashtags 보이게 수정

array안의 element 하나하나 ‘hashtags’로 들어가고, li로 정렬되어 촤르륵 나온다.

//video.pug mixin video(video) div h4 a(href=`/videos/${video.id}`)=video.title p=video.description ul each hashtags in video.hashtags li=hashtags small=video.createdAt hr

#6.25 Delete Video

Step 1. watch.pug에서 링크 만들어주기

URL에 들어가면 지워지게 만들거임.

//watch.pug extends base.pug block content div p=video.description small=video.createdAt a(href=`${video.id}/edit`) Edit Video → br a(href=`${video.id}/delete`) Delete Video →

Step 2. Router 와 Controller 만들기

//videoRouter.js videoRouter.route("/:id([0-9a-f]{24})/delete").get(deleteVideo); //videoController.js export const deleteVideo = async (req, res) => { // URL에 있는 id를 가져옴 const { id } = req.params; // findOneAndDelete {_id : id} 와 같음 await Video.findByIdAndDelete(id); return res.redirect("/"); };

특별한 이유가 있지 않은 이상 findByIdAndRemove 대신 findByIdAndDelete를 쓰는 게좋다

#6.26 Search part One

정렬

mongoose는 굉장히 훌륭한 쿼리엔진을 갖고 있음 문서들을 보여주는 방식을 수정할 수 있다 어떻게 검색하고 정렬할지 정할 수 있음.

export const home = async (req, res) => { //.sort({}) : 무엇을 기준으로 할지 선택하고 "asc", "desc" 으로 오름차순, 내림차순 결정 const videos = await Video.find({}).sort({ createdAt: "desc" }); return res.render("home", { pageTitle: "Home", videos }); };

Search

globalrouter, videoController 생성

//globalRouter.js globalRouter.get("/search", search); //videoController.js export const search = (req, res) => { return res.render("search"); };

search.pug 생성

//search.pug extends base.pug block content form(method="GET") input(placeholder="Search by title",name="keyword" type="text") input(value="Search now", type="submit")

*name이 없을때와 있을때 submit 제출결과*input에 name을 지정해주지 않으면, submit 제출해도 URL에 변화가 없음!

form 정보 가져오기

req.query 안에 keyword 정보가 들어있음

export const search = (req, res) => { const { keyword } = req.query; if (keyword) { // serach } return res.render("search", { pageTitle: "Search" }); };

(req.query와 req.params의 차이?)

req.query

Get한 URL에서 q가 가리키는 값을 가져옴

/ GET /search?q=tobi+ferret console.dir(req.query.q) // => 'tobi ferret'

req.params

GET한 URL에서 미리 지정해둔 name에 해당하는 URL값을 가져옴

// GET /user/tj console.dir(req.params.name); // => 'tj'

#6.27 Search part Two

검색 기능 만들기

밖에서 videos를 선언해주고, 안에서 업데이트 해준다. 그후 search.pug로 videos 정보를 보낸다

//videoController.js export const search = async (req, res) => { const { keyword } = req.query; let videos = []; if (keyword) { // title이 keyword가 일치하는 경우를 찾아서 array로 만듬. videos = await Video.find({ title: keyword, }); } return res.render("search", { pageTitle: "Search", videos }); };

search.pug 생성

//search.pug extends base.pug include mixins/video block content form(method="GET") input(placeholder="Search by title",name="keyword" type="text") input(value="Search now", type="submit") div each video in videos +video(video)

Regular Expression

^ : 앞에 오는 단어만 검색

$ : 마지막 단어만 검색

i : 대소문자 구분 없앰

Regular Expression 적용

//videoController.js export const search = async (req, res) => { const { keyword } = req.query; let videos = []; if (keyword) { videos = await Video.find({ title: { //keyword를 contain하고 대소문자 구분없이 찾게됨. (mongoDB의 기능) $regex: new RegExp({ keyword }, "i"), }, }); } return res.render("search", { pageTitle: "Search", videos }); };

[정규표현식]

[몽고DB regex]

[RegExp mdn]

[정규표현식 참고사이트]

#6.28 Conclusions

CRUD 완성

이 섹션은 Mongoose와 친해지기 위한 목적

내가 정리해놓은게 너무 하찮아서 거의 다 이분거 보고 다시적었다.. 7장부터는 신경써서 정리했으니 괜찮겠지,, ㅠ

https://onlee3.github.io/clonecoding/WetubveC6/#60-array-database-part-one

from http://til-blog.tistory.com/17 by ccl(A) rewrite - 2021-12-24 22:27:57