909 Devlog

[유튜브 클론코딩] mongoDB - 기초부터 연결까지 본문

토이 프로젝트/유튜브 클론코딩

[유튜브 클론코딩] mongoDB - 기초부터 연결까지

구공구 2023. 7. 24. 22:33
728x90

1. Array Database

데이터 베이스에 대해 배우기 전에, 백엔드에 데이터를 어떻게 보내는지 먼저 보고 넘어갑시다.

 

이때까지 작성했던 Router.js 파일들을 보면 get 함수만 사용하고 있었습니다.

이제 post를 통해 데이터를 보내봅시다.

 

그전에, 이전 포스팅에서 videoController.js 파일의 trending 함수 내부에 const videos 배열을 작성했었습니다.

모든 controller에 배열을 사용하기 위해 배열을 밖으로 꺼내고 let으로 바꿔줍니다.

// videoController.js

let videos = [
    {
        title: "First Video",
        rating:5,
        commnets:2,
        createdAt:"2 minutes ago",
        views:59,
        id:1,
    },
    {
        title: "Second Video",
        rating:5,
        commnets:2,
        createdAt:"2 minutes ago",
        views:59,
        id:2,
    },
    {
        title: "Third Video",
        rating:5,
        commnets:2,
        createdAt:"2 minutes ago",
        views:59,
        id:3,
    }
    ];

export const trending = (req, res) => {
    res.render("home", {pageTitle: "Home", fakeUser:fakeUser, videos});
};

...

이제 홈페이지에서 비디오를 하나 클릭하면 그 비디오를 볼 수 있는 페이지로 넘어가서 해당 비디오를 볼 수 있게 만들어봅시다.

 

링크를 만들기 위해 video.pug를 수정해 줍니다.

// video.pug

mixin video(info)
    div
        h4
            a(href=`/videos/${info.id}`)=info.title // <-- a 수정
        ul 
            li #{info.rating}/5.
            li #{info.comments} comments.
            li Posted #{info.createdAt}.
            li #{info.views} views.

만약 이전 포스팅에서 fakeUser 데이터를 보내셨다면 a링크를 클릭하고 watch 페이지로 넘어가면 오류가 발생할 것입니다.

watch controller에 fakeUser 데이터를 보내주지 않아서 발생하는 오류인데, watch contoller에 fakeUser 데이터를 보내주시거나, 앞으로 fakeUser를 쓸 일이 없으니 관련된 모든 코드를 지워주시면 오류가 없어질 것입니다. (videoController.js의 fakeUser객체, trending 함수에 보내는 fakeUser, base.pug의 if else 문)

 

a 태그를 보시면 이때까지 Pug에서 사용하던 #{}가 아닌 일반 자바스크립트에서 사용하는 ${}을 사용했습니다.

li에서 사용하는 것처럼 문자열에 사용하려면 #{}을 사용해야 하지만, href, class, id 같은 속성에 사용할 때는 ${}을 `(백틱)으로 감싸서 사용해야 합니다.

 

이제 링크를 클릭하시면 해당 비디오를 볼 수 있는 watch 페이지로 넘어갈 수 있습니다 하지만 아직 해당 비디오가 화면에 뜨지 않으니, 뜨도록 코드를 작성해 봅시다.

// videoController.js

...

export const watch = (req, res) => {
    const id = req.params.id;
    // const {id} = req.params; ES6 문법
    const video = videos[id - 1];
    return res.render("watch", {pageTitle: `Watching ${video.title}`});
};

...

 

이제 아래 URL에 접속하면,

URL id 부분에 입력되어 있는 비디오의 id에 맞는 비디오 정보가 화면에 뜰 것입니다.

하지만 video의 제목만 뜨면 화면이 밋밋하니 video 객체를 보내서 video의 다른 정보들도 표시해 봅시다.

 

videoController.js에서 response에 video 객체를 추가로 보내주고

// videoController.js

...

export const watch = (req, res) => {
    const id = req.params.id;
    // const {id} = req.params; ES6 문법
    const video = videos[id - 1];
    return res.render("watch", {pageTitle: `Watching ${video.title}`, video}); // <-- video 객체 추가
};

...

watch.pug 코드를 다음과 같이 수정합니다.

// watch.pug

extends base.pug

block content 
    h3 #{video.views} views

이제 비디오의 조회수가 나오는데, 만약 비디오의 조회수가 1이면 view이고, 2 이상일 때는 views로 표시해야 영어 문법에 맞게 출력할 수 있지 않을까요?

1.1 Pug 파일에서 삼항연산자 사용

이전 포스팅에서 Pug의 #{}에는 자바스크립트 코드를 쓸 수 있다고 봤었습니다.

이번 기회에 한번 더 연습해 봅시다.

 

// watch.pug

extends base.pug

block content 
    h3 #{video.views} #{video.views === 1 ? "view" : "views"}

watch.pug 코드를 위와 같이 수정하면, views에 따라 뒤에 출력되는 문자열을 다르게 출력할 수 있습니다.

 

이제 post를 연습하기 위한 마지막 준비를 해봅시다.

 

1.2 URL

영상 수정을 위한 페이지를 만들기 위해 watch.pug에 링크를 만들어 영상 수정 페이지로 이동할 수 있도록 코드를 작성해 봅시다.

// watch.pug

extends base.pug

block content 
    h3 #{video.views} #{video.views === 1 ? "view" : "views"}
    a(href=`${video.id}/edit`) Edit Video &rarr;

a태그의 href를 보면 URL이 특이한 것을 볼 수 있습니다.

여기서 URL에 대해 한번 짚고 넘어가면 좋을 것 같네요.

 

URL의 경로는 절대 경로와 상대 경로가 있습니다.

URL의 처음에 "/"가 붙어있다면 절대 경로가 되고,

없다면 상대 경로가 됩니다.

 

절대 경로의 "/"는 Root를 나타내며, 이번 프로젝트에서 "/"는 최상위 디렉토리인 "localhost:4000/"를 가리키게 됩니다.

즉, 위 코드와 다르게 "/"를 붙여 "/${video.id}/edit"으로 URL을 지정하면 저희가 의도하는 URL("localhost:4000/videos/${video.id}/edit")이 아닌 "localhost:4000/${video.id}/edit" 주소로 이동하게 되어 오류가 발생합니다.

 

하지만 위 코드처럼 "/"를 붙이지 않고, 상대 경로를 사용한다면 현재 watch 페이지에 들어와 있는 상태이니 "localhost:4000/videos"에다가 "${video.id}/edit"가 추가된 "localhost:4000/videos/${video.id}/edit"으로 이동하게 되니 정상적으로 작동하게 되는 것입니다.

 

절대 경로와 상대 경로 모두, 사용 방법을 알고 있다면 매우 편리하게 URL 주소 지정을 할 수 있습니다.

예를 들어 그냥 "localhost:4000/login"에 접속하고 싶으면 "/login"을 사용하면 되고

 

만약 "localhost:4000/apple/potato/mango/cherry"에 접속한 상태에서 "localhost:4000/apple/potato/mango/cherry/kiwi"에 접속하고 싶으면 "kiwi"를 사용하면 됩니다.

1.3 POST

이제 videoController에서, watch controller에 했던 것처럼 edit controller도 어떤 비디오를 수정하는지 알아야 하기 때문에 edit에도 같은 작업을 해주겠습니다.

// videoController.js
...

export const edit = (req, res) => {
    const {id} = req.params;
    const video = videos[id - 1];
    return res.render("edit", {pageTitle: `Editing ${video.title}`, video});
};

...

edit.pug에 비디오를 수정하기 위한 form을 작성해 주겠습니다.

// edit.pug

extends base.pug

block content 
    h5 Change Title of video
    form(method="POST") 
        input(name="title", placeholder="Video Title", value=video.title, required)
        input(value="Save", type="submit")

그럼 아래와 같은 화면이 출력될 것입니다.

이제 Save를 눌러보면 오류가 발생합니다.

예전에 봤던 Cannot GET 오류와 비슷한 오류입니다.

 

만약 그냥 데이터를 받는 게 목적이라면, get을 사용하고

받은 데이터로 데이터 베이스를 수정하거나 삭제하는 등 데이터 베이스에 접근하려면 post를 사용합니다.

 

videoController에 post에 사용할 함수를 작성하고

// videoController.sj
...

export const getEdit = (req, res) => {
    const {id} = req.params;
    const video = videos[id - 1];
    return res.render("edit", {pageTitle: `Editing : ${video.title}`, video});
};
export const postEdit = (req, res) => {
    return res.end();
};

videoRouter에 post함수를 작성합니다.

// videoRouter.js

...

videoRouter.get("/:id(\\d+)/edit", getEdit);
videoRouter.post("/:id(\\d+)/edit", postEdit);
// videoRouter.route("/:id(\\d+)/edit").get(getEdit).post(postEdit); <-- 한 줄로 줄여서 사용할 수 있습니다.

...

이제 서버는 post method를 이해하여 res.end()가 작동되고 response가 끝나게 됩니다.

아무런 변화가 없지만, 그래도 서버가 post method를 이해하긴 했습니다.

 

res.end() 대신에 res.redirect()를 사용해 봅시다.

res.redirect()는 브라우저가 자동으로 이동하도록 합니다.

// videoController.js

...

export const postEdit = (req, res) => {
    const {id} = req.params;
    console.log(req.body);
    return res.redirect(`/videos/${id}`);
};

이제 save 버튼을 누르면 수정한 영상을 보는 페이지로 이동하고, req.body를 console.log 합니다.

여기서 body는 req.params 처럼 req 객체에 존재하는 프로퍼티입니다.

지금은 body가 아래처럼 undefined로 뜨는 게 정상입니다.

이제 post를 통해 form의 데이터가 전송되고 있는데, 어떻게 데이터를 받을까요?

 

express에는 urlencoded라는 메서드가 존재합니다.

urlencoded는 body를 이해할 수 있도록 도와주는데

urlencoded의 프로퍼티 중에 extended가 body에 있는 정보들을 보기 좋게 형식을 갖춰주는 일을 합니다.

urlencoded를 거치지 않고, req.body를 console.log 해보면 위 사진처럼 undefined가 뜨게 됩니다.

 

server.js에 urlencoded를 추가해 봅시다.

// server.js
...

app.use(loggerMiddleware);
app.use(express.urlencoded({extended: true})); // <-- urlencoded 추가

app.use("/", globalRouter);

...

이제 다시 save를 눌러보면 수정한 영상을 보는 페이지로 이동하고, req.body가 우리가 이해할 수 있게 출력됩니다.

videoController.js에 id를 저장했던 것처럼, title을 저장하고, 기존 title을 수정해 봅시다.

// videoController.js

...

export const postEdit = (req, res) => {
    const {id} = req.params;
    const {title} = req.body;
    videos[id - 1].title = title;
    return res.redirect(`/videos/${id}`);
};

드디어 save 버튼을 누르면 videos 객체의 title을 바꿀 수 있게 되었습니다.

 

이렇게 해서, 어떻게 data를 얻고, 어떻게 data를 post 하는지 이해하게 되었으며, form을 만들 때 req.body로부터 data를 받는다는 것을 알게 되었습니다.

 

이제 진짜 데이터 베이스를 다뤄 봅시다.

2. mongoDB

mongoDB는 document 기반 NoSQL 데이터베이스 시스템입니다. JSON 형태의 동적 스키마형 문서를 사용하여 테이블 형태를 사용하지 않고 SQL을 사용하지 않습니다.

 

mongoDB 설치에 대해서는 다른 분 블로그에 잘 설명되어 있으니 보시고 설치하시면 될 겁니다.

 

mongoDB를 설치하셨다면, mongoose도 설치해 줍니다.

mongoose는 자바스크립트를 사용하여 mongoDB를 쓸 수 있도록 해주는 도구입니다.

 

mongoose를 설치하기 전에, mongoDB가 제대로 설치되었는지 확인해 봅시다.

사용하시는 OS의 터미널에서 아래와 같이 mongod 라고 입력하고,

다음과 같은 결과가 나오면 잘 설치하신 겁니다.

혹시 명령어가 계속 실행 중이라면 윈도우 기준 Ctrl + C를 눌러 실행을 중지시킬 수 있습니다.

 

이제 mongoose를 설치해 봅시다.

프로젝트 파일을 VSC로 열고, 터미널에서 다음과 같이 입력하고 실행합니다.

npm i mongoose

이제 server.js파일이 있는 src 폴더에 db.js 파일을 만들어 줍니다.

이제 컴퓨터에 실행되고 있는 mongoDB에 연결을 해줘야 합니다.

db.js 파일에 다음과 같이 작성합니다.

// db.js

import mongoose from "mongoose";

mongoose.connect("mongodb://127.0.0.1:27017/nodetest");

mongoose.connect()의 'nodetest' 부분은 데이터베이스 이름을 정하는 부분으로, 원하시는 이름을 적어주시면 됩니다.

그리고 server.js 맨 위에 db.js 파일 자체를 import 해 줍니다.

// server.js

import "./db";

서버가 위 코드를 보고 db파일을 import 해줌으로써 서버가 mongoDB에 연결되는 것입니다.

mongoDB 연결의 성공 여부나 에러를 console.log로 출력하게 하고 싶으시다면 아래와 같이 코드를 작성하시면 됩니다.

// db.js

import mongoose from "mongoose";

mongoose.connect("mongodb://127.0.0.1:27017/nodetest");

const db = mongoose.connection;

db.on("error", (error) => console.log("DB Error", error));
db.once("open", () => console.log("Connected to DB"));

2.1 Schema

이때까지 배열로 만든 데이터를 사용했으니, 이제 mongoDB를 사용해서 데이터를 사용하기 위해 먼저

src 폴더에 models 폴더를 생성하고 그 안에 Video.js 파일을 생성합니다.

Video.js는 DB에 비디오의 정보를 담은 비디오 schema을 저장합니다.

schema는 mongoose에서 지원하는 기능인데, 데이터베이스에게 비디오가 어떻게 생겼는지 설명하는 기능을 합니다.

Video.js에 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 VideoModel = mongoose.model("Video", videoSchema);
export default VideoModel;

위 코드를 보면, videoSchema는

  • String 형식으로 된 title과 description이 존재하며,
  • Date 형식으로된 createdAt과
  • String 배열 형식인 hashtags,
  • Number 형식인 views와 rating을 가진 meta가 존재합니다.

이런 식으로 저장될 데이터에 대한 형식을 선언합니다.

이제 형식을 선언했으니, model을 정의하고 controller에서 사용하기 위해 export 하고, server.js에 db.js를 불러온 코드 밑에 Video.js를 불러옵시다.

// server.js

import "./db";
import "./models/Video";

...

 

mongoose를 사용함에 따라, server.js에 DB관련된 코드들이 늘어날 것입니다.

코드들을 분류하기 위해 express 관련 코드만 server.js에 두고, init.js에 그 외 코드들을 정리해 봅시다.

server.js 파일이 있는 같은 폴더에 init.js를 생성하고 server.js에 있는 일부 코드를 옮깁니다.

init.js에도 app이 필요하기 때문에, server.js 맨 밑에 export default app;을 작성해 줍니다.

// init.js

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

const PORT = 4000;

app.listen(PORT, () => console.log(`Server listening on port http://localhost:${PORT}`));

그럼 이제 server.js는 app을 export 할 뿐 작동시키지는 않기 때문에, 프로젝트 시작 파일을 init.js로 옮겨줍시다.

package.json의 script에서 server.js를 init.js로 수정하기만 하면 다시 정상적으로 작동할 것입니다.

 

이제 videoController.js에서 연습용으로 사용하던 가짜 데이터와 관련된 모든 코드를 지웁시다.

모두 정리한 코드는 다음과 같습니다.

export const trending = (req, res) => {
    res.render("home", {pageTitle: "Home"});
};
export const watch = (req, res) => {
    const {id} = req.params;
    return res.render("watch", {pageTitle: `Watching` });
};
export const getEdit = (req, res) => {
    const {id} = req.params;
    return res.render("edit", {pageTitle: `Editing`});
};
export const postEdit = (req, res) => {
    const {id} = req.params;
    const {title} = req.body;
    return res.redirect(`/videos/${id}`);
};

export const getUpload = (req, res) => {
    return res.render('upload', {pageTitle: "Upload Video"});
};

export const postUpload = (req, res) => {
    return res.redirect('/');
};

2.2 real DB

이제 video model을 사용해 봅시다.

 

DB에서 데이터를 받고 사용자에게 데이터를 보내주는 역할은 controller가 하고 있기 때문에 videoController.js 파일의 맨 위에 Video.js에서 export 했던 VideoModel를 불러옵시다.

 

videoController.js의 trending 함수에 쿼리문을 사용해 봅시다.

VideoModel.find()는 콜백함수를 사용하는 방법과 promise를 사용하는 방법이 있습니다.

 

콜백함수는 우리가 이때까지 코드를 작성하면서 많이 사용했었는데,

callback이란 무언가가 발생하고 난 다음 호출되는 function을 말합니다. 자바스크립트에서 기다림을 표현하는 하나의 방법으로 생각할 수 있습니다.

 

이러한 callback과 곧 설명해 드릴 promise를 사용하는 이유는 실행과 동시에 적용되지 않는 코드들이 존재하기 때문입니다. 예를 들어 port에 연결하거나, 데이터베이스에서 데이터를 가져오는 등의 코드가 즉시 적용되지 않기 때문에 잠깐 기다려야 합니다. 적용에 1 마이크로초가 걸린다고 해도 기다리긴 기다려야 하니 callback과 promise 같은 기다림을 명령하는 코드를 사용합니다.


** mongoose가 업데이트되면서 find()에 콜백함수를 지원하지 않도록 변경되었습니다. 그래도 기존 콜백함수 방법을 통해 예시를 보시면 어떤 것인지 감이 오실테니 남겨놓겠습니다.**

 

일단, 이때까지 사용했던 callback을 사용해 어떤 느낌인지 한번 봅시다.

VideoModel.find() 안의 빈 객체는 모든 형태의 비디오를 찾는다는 의미이고, 

그 뒤에 있는 콜백함수의 매개변수는 각각 에러의 종류와 불러온 비디오들을 의미합니다.

 

각각을 console.log 해서 알아봅시다.

 

// videoController.js
...

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

...

다음과 같이 출력됩니다.

출처 : 노마드코더 유튜브 클론코딩 강의

출력 결과는 특이하게도 hello가 먼저 출력되고 errors와 videos가 출력되었습니다.

 

페이지를 request 해서 hello가 출력되었고,

request가 완성되고 render 과정을 거친 뒤에  logger가 출력되었으며,

render와 response 과정 이후에 errors와 videos가 출력되었습니다.

 

이와 같이 데이터베이스에서 데이터를 찾으려면 코드 순서를 render 전에 뒀어도 위의 순서를 따라야 해서 기다려야 하는 것입니다.

따라서 이 결과는 자바스크립트가 아무런 에러 없이 DB와 통신했다는 것을 보여줍니다.


2.3 promise

이제 promise를 알아봅시다.

export const trending = async (req, res) => {
    console.log("I Start");
    const videos = await VideoModel.find({});
    console.log("I Finish");
    console.log(videos);
    res.render("home", {pageTitle: "Home", videos: [] });
};

위의 콜백함수를 사용했던 것과 다른 점은 trending에 async를 선언해 주었고,

VideoModel.find() 앞에 await를 선언했으며,

find 함수에 콜백함수 없이 검색 조건만 전달했습니다.

 

VideoModel.find() 앞에 await을 선언하면 find는 callback을 필요로 하지 않습니다.

await은 규칙상 function 안에서만 사용이 가능하며, 그 function이 asynchronous(async) 일 때만 가능합니다.

async-await은 상당히 직관적인 최신 문법으로써 자바스크립트가 어디서 기다리는지 쉽게 확인할 수 있습니다.

 

출력 결과는 다음과 같습니다.

I Start가 출력되고,

데이터베이스에서 비디오를 찾고

I Finish가 출력되었으며,

찾은 비디오가 출력되었습니다.

 

그럼 callback 함수를 사용했을 때 얻은 errors는 어떻게 출력할까요?

이때 try, catch문이 사용됩니다.

 

말 그대로 무언가를 try 하고 에러가 있다면 그 에러들을 catch 합니다.

다음과 같이 사용합니다.

export const trending = async (req, res) => {
    try{
        console.log("I Start");
        const videos = await VideoModel.find({});
        console.log("I Finish");
        console.log(videos);
        res.render("home", {pageTitle: "Home", videos: [] });
    } catch(error) {
        return res.render("server-error", {error});
    }
};

if else문과 같이 if(try)를 실행하고 에러(데이터베이스가 꺼지거나, 연결이 끊겼거나, 사용자가 몰려 포화상태이거나 등등)가 발생한다면 else(catch)를 실행합니다.

 

잘 실행되는 것을 확인하셨다면, 확인용으로 작성해 놓은 console.log를 지웁시다.

2.4 return and render

그런데, 위 코드에서 뭔가 이상한 점을 느끼셨나요?

 

항상 써왔던 return이 try문 내부에 없는데도 불구하고 코드가 정상적으로 작동하는 것을 볼 수 있습니다.

 

그럼 render 함수를 두 번 호출하면 어떻게 될까요?

export const trending = async (req, res) => {
    try{
        const videos = await VideoModel.find({});
        console.log("render once");
        res.render("home", {pageTitle: "Home", videos: [] });
        console.log("render again");
        res.render("home", {pageTitle: "Home", videos: [] });
    } catch(error) {
        return res.render("server-error", {error});
    }
};

브라우저는 정상적으로 출력되었지만 터미널에 오류로그가 뜨게 되었습니다.

"render once"가 실행되고 render() 함수가 실행되었습니다. 그다음으로 "render again"이 실행되었고 그 다음으로 오류가 뜨게 되었습니다.

오류 로그는 이미 render 한 것은 다시 render 할 수 없음을 말해주고 있습니다. render 함수가 아닌 다른 함수를 사용하더라도 같은 오류가 뜨게 됩니다.

 

그럼 한번 return을 사용해 봅시다.

코드를 다음과 같이 수정합니다.

export const trending = async (req, res) => {
    try{
        const videos = await VideoModel.find({});
        console.log("render once");
        return res.render("home", {pageTitle: "Home", videos: [] });
        console.log("render again");
        res.render("home", {pageTitle: "Home", videos: [] });
    } catch(error) {
        return res.render("server-error", {error});
    }
};

브라우저를 새로고침 하면 화면 출력이 정상적으로 되고,

터미널에도 오류가 뜨지 않은 것을 볼 수 있습니다.

 

return은 해당 function을 종료시켜 주는 기능이 있습니다.

따라서 return 뒤에 작성되어 있는 "render again"과 render 함수는 동작을 하지 않습니다. 어떤 일이 있어도 함수 내에서 return 뒤의 코드는 절대 동작하지 않습니다.

 

마지막으로 trending 함수를 정리하고 이번 포스팅을 마치겠습니다.

export const trending = async (req, res) => {
    const videos = await VideoModel.find({});
    return res.render("home", {pageTitle: "Home", videos});
};

굳이 error를 받을 필요는 없으니 try catch 문도 삭제했습니다.

 

다음 포스팅에서 CRUD를 하면서 DB 실습을 해보겠습니다.

728x90