유튜브 클론코딩 복습노트-5

Hyuno Choi·2021년 7월 27일
0
post-thumbnail

2021년 7월 28일

지난 시간에는 데이터베이스가 있다고 가정하고 비디오 업로드, 수정, 시청 등의 기능을 간단하게 구현해보았습니다. 이번 포스팅부터는 실제 데이터베이스를 구현해보겠습니다. 이 프로젝트에서는 MongoDB 를 사용합니다.

MongoDB 기초 셋업⚙️

MongoDB 설치

우선 MongoDB를 설치하기 위해 홈페이지로 이동합니다. https://www.mongodb.com

그리고 네이게이션 바에서 [Docs - Server] 로 들어갑니다.

그리고 왼쪽 네비게이션 바의 [Installation - Install MongoDB Community Editon] 을 클릭합니다. 커뮤니티 에디션의 경우 무료로 사용할 수 있습니다.

그리고 나오는 페이지에서 자신의 OS 환경에 맞는 링크로 들어갑니다.

저는 macOS 환경에서 프로젝트를 진행하고 있으므로 해당 링크로 들어가겠습니다. macOS에서는 brew 를 통해 MongoDB를 설치하게 됩니다. 만약 brew 가 설치되어있지 않다면 페이지의 안내에 따라 brew 를 먼저 설치해주세요.🍺

brew 가 설치되었다면 커맨드 라인 사용을 위해 $ Xcode command-line tools 를 다운받아야 합니다. 이 툴은 본인의 macOS에서 개발환경을 이미 갖췄다면 설치되어 있을 수도 있습니다.

이제 brew 가 다운받을 수 있는 레포지토리에 MongoDB 레포지토리를 확장시켜줘야합니다. $ brew tap mongodb/brew 명령어를 실행합니다.

이제 마지막으로 $ brew install mongodb-community@5.0 명령어를 통해 MongoDB를 설치합니다. 2021년 7월을 기준으로 5.0 버전이 안내되고 있습니다. 명령어 맨 끝의 버전은 다를 수 있습니다.

설치가 끝나고 $ mongod 명령어를 입력했을 때 데이터들이 출력된다면 설치가 정상적으로 끝난 것입니다.

DB 서버 시작 & 종료

맨 처음 설치를 마치고 $ mongo 명령어를 입력하면 이런 오류메시지가 출력됩니다.

Error: couldn't connect to server 127.0.0.1:27017, connection attempt failed: SocketException: Error connecting to 127.0.0.1:27017 :: caused by :: Connection refused :

아직 MongoDB를 설치하기만 하고 DB 서버를 실행해주지 않아서 그렇습니다. MongoDB의 사용은 모두 로컬 DB 서버와의 상호작용으로 이루어집니다. MongoDB 서버를 다음 명령어로 켜줍니다. $ brew services restart mongodb-community 성공했다는 메시지가 출력되면 서버가 정상적으로 켜진 것입니다.

저사양 환경에서 개발을 진행할 경우 백그라운드에서 서버를 계속 돌리고 있는 것이 부담이 될 수 있습니다. macOS에서는 top 명령어를 통해 DB 서버의 프로세스 식별 번호(PID)를 얻어 $ kill <PID> 명령어로 서버를 종료하는 방법과 mongo shell 의 명령어를 사용하는 방법이 있습니다.

그중 조금 더 직관적인 두 번째 방법으로 서버를 종료해보겠습니다. $ mongo 명령어로 mongo shell 에 진입합니다. 그리고 use admin 명령어로 어드민 권한을 얻습니다. 그리고 db.shutdownServer() 명령어로 서버를 종료합니다. 마지막으로 exit 를 입력해 쉘에서 나오면 끝입니다. 이미 서버와의 연결은 끊긴 상태이므로 에러메시지가 출력되니 신경쓰지 않아도 됩니다.

다음번에 다시 DB를 사용해 개발을 진행할 때 다시 시작 명령어를 입력해주세요.

mongoose를 통한 Node.js 연결

mongoose 설치

MongoDB를 Node.js에서 사용하기 위해서는 mongoose를 추가로 설치해야 합니다. mongoose는 NPM 모듈이므로 NPM 명령어를 통해 설치합니다. npm i mongoose 를 실행해주세요. packge.jsonDependencies 목록에 정상적으로 추가되었다면 설치가 끝난 것입니다.

db.js 파일 생성

이제 본격적으로 MongoDB를 사용해보겠습니다. $ brew services restart mongodb-community 명령어로 DB 서버를 대기시켜주세요. 그리고 프로젝트 폴더 내의 src 폴더 안에 db.js 파일을 생성합니다. 파일명은 데이터베이스를 담당한다는 것만 알 수 있게 지어주면 됩니다.

DB 서버 연결

이제 db.js 에서 mongoose를 불러옵니다.

import mongoose from "mongoose";

그리고 설치한 MongoDB 서버와 mongoose를 연동합니다. 이를 위해 먼저 local DB 서버의 주소를 알아야 합니다. 터미널에 $ mongo 를 입력해주세요.

MongoDB shell version v5.0.1
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("620d175a-34a7-46bd-9f9a-a4377a9207c2") }
MongoDB server version: 5.0.1

DB에 오류가 없다면 이런 메시지가 출력됩니다. 여기서 connecting to: mongodb://127.0.0.1:27017/? 라는 부분이 우리가 알아야 할 서버 주소입니다. connection to: 이후부터 주소를 복사합니다.

mongoose.connect(서버 주소/프로젝트명)을 통해 DB와 연결합니다. db.js 에 다음 내용을 추가해주세요.

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

이제 db.js 파일을 server.js 파일에서 import 하여 사용할 수 있도록 합니다. server.js 파일 윗줄에 import 문을 추가합니다.

import ./db;

그리고 $ npm run dev 명령어로 서버를 시작합니다. 서버를 시작하면 콘솔에 경고 메시지가 출력됩니다.

(node:22509) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true } to MongoClient.connect.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:22509) [MONGODB DRIVER] Warning: Current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.

connect()를 사용할 때 디폴트로 지정된 옵션들이 추후 업데이트에서 사라질 예정인가봅니다. 경고 메시지에서 알려준대로 최신 옵션을 적용해보겠습니다.

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

이렇게 저장하고 서버를 재시작하면 더 이상 경고 메시지가 출력되지 않습니다. 해당 경고 메시지는 현재 포스팅하고 있는 시점의 버전에서만 나타날 수 있으니, 버전이 다르다면 해당 버전에 맞게 초기 세팅을 해주세요.

이벤트 핸들러 구현

서버가 시작될 때 서버가 구동한다는 메시지와 함께 포트 번호를 출력했던 것처럼 MongoDB에 연결될 때와 에러가 발생했을 때 해당 메시지를 콘솔에 출력할 수 있는 핸들러를 구현해보겠습니다.

그러기 위해 우선 connection 인스턴스를 생성합니다.

import mongoose from "mongoose";

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

const db = mongoose.connection; // connection 인스턴스 생성

그리고 에러 핸들러와 연결 핸들러를 각각 만들어서 db 에 달아줍니다.

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("Connect to DB 🦢");
};

const handleError = (error) => {
  console.log("DB Error", error);
};

db.on("error", handleError); // error가 발생하면 콜백 실행
db.once("open", handleOpen); // DB에 연결될 때 한 번만 콜백 실행

db.on() 은 마치 이벤트 리스너처럼 해당되는 이벤트가 발생하면 콜백을 실행합니다. 여기서는 error 가 발생하면 handleError 를 실행하도록 했습니다.

db.once() 는 해당 이벤트가 처음 일어났을 때 한 번만 실행됩니다. DB가 성공적으로 연결되면 해당 메시지를 출력해주도록 했습니다. 파일을 저장하면 DB 연결 메시지가 정상적으로 출력됩니다.

Video 모델 만들기

비디오를 데이터베이스에 저장하기 위해서는 먼저 비디오가 어떤 형태로 이루어진 데이터인지 정의해주어야 합니다. src 폴더 내에 models 폴더를 만듭니다. 이 폴더 안에는 데이터베이스에 저장할 데이터의 모델을 정의하는 파일이 들어가게 됩니다. 그리고 models 폴더 안에 Video.js 파일을 생성합니다.

모델을 만들기 위해 mongoose를 불러옵니다.

import mongoose from "mongoose";

이제 비디오 데이터의 구조를 설계합니다. mongoose에서 모든 모델은 해당 모델의 스키마를 만드는 것부터 시작합니다. 스키마는 스키마 이름과 스키마 타입 쌍으로 사용합니다.

import mongoose from "mongoose";

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

new mongoose.Schema() 에 스키마와 타입을 쌍으로 묶어 객체 형태로 전달합니다. createdAt: Date 에서 볼 수 있듯이 프로그래밍 언어에서 사용하는 일반적인 자료형과 mongoose 스키마 타입은 다릅니다. mongoose에서 사용할 수 있는 데이터 타입 리스트는 아래와 같습니다.

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
  • Decimal128
  • Map

각각의 데이터 타입에 관한 자세한 설명은 이 링크를 참고해주세요. https://mongoosejs.com/docs/guide.html

또한 hashtags: [{ type: String }] 처럼 문자열의 리스트로 데이터가 저장되게 스키마를 설계할 수도 있고, meta 처럼 스키마를 중복해서 설계할 수도 있습니다.

이제 만들어진 스키마 구조를 가지고 mongoose 모델을 만들고, 최종적으로 모델을 export 해줍니다.

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;

mongoose.model() 에 모델 이름과 스키마 구조를 인자로 넘겨줍니다.

코드 파일 정리

server.js 파일 분할

이제 만들어진 모델을 import "./models/video"; 를 통해 서버에서 사용할 수 있도록 불러옵니다. 그런데 server.js 파일에 데이터베이스 관련 코드를 쓰고 싶지는 않습니다. init.js 파일을 새로 만들어서 초기화와 관련된 모든 부분을 다루도록 하고, 서버 파일은 서버 구동과 관련된 부분만을 다루도록 하겠습니다. src 폴더 내부에 init.js 파일을 생성합니다.

그리고 서버 시작을 담당하는 코드를 복사해 init.js 파일에 넣습니다.

import app from "./server";

const PORT = 4000;

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

app.listen(PORT, handleListening);

물론 app 어플리케이션을 사용할 수 있도록 server.js 에서 export 해줘야 합니다. 그리고 데이터베이스 및 모델을 불러오는 import 문도 여기에 넣습니다.

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

const PORT = 4000;

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

app.listen(PORT, handleListening);

이제 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";

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("/users", userRouter);
app.use("/videos", videoRouter);

export default app;

그리고 package.json 파일의 script 부분에서 $ npm run dev 명령어를 통해 실행하고자 하는 파일을 server.js 에서 init.js 로 바꿔줍니다.

"scripts": {
    "dev": "nodemon --exec babel-node src/init"
  },

이제 $ npm run dev 명령어를 실행하여 서버가 정상적으로 작동하는지 확인합니다.

연습용 데이터 코드 삭제

이제 진짜 데이터베이스가 생겼기 때문에 가짜 videos 객체는 필요 없습니다. videoContoller.js 파일에서 videos 객체 및 해당 객체를 사용하는 코드를 모두 제거합니다.

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;
  return res.redirect(`/videos/${id}`);
};

export const getUpload = (req, res) => {
  return res.render("upload");
};

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

비디오 가져오기 기능 구현

비디오 만들기 기능을 구현하기 전에 비디오 가져오기 기능을 먼저 구현하겠습니다. 이렇게 되면 나중에 비디오를 만들었을 때 저절로 홈 화면에 표시될 것입니다. videoContoller.jstrending 컨트롤러에 작업을 해주겠습니다.

우선 pug 파일과 이름을 맞추기 위해 home 으로 컨트롤러 이름을 바꿉니다. 그리고 비디오 모델을 사용하기 위해 모델 파일을 import 해줍니다.

import Video from "../models/video"

이제 데이터베이스에 저장된 비디오를 불러오는 코드를 작성합니다. Video 모델로 만들어진 데이터를 찾으려면 Video.find() 를 사용합니다. find() 안에는 쿼리를 스키마 형태로 전달할 수 있습니다. 예를 들어 비디오 제목이 "first"인 비디오 데이터를 찾고 싶다면 이렇게 쿼리를 전달할 수 있습니다.

Video.find({
  title: "first"
});

지금은 모든 비디오(아직은 데이터가 없지만)를 불러오게 하겠습니다. 쿼리에 빈 객체를 전달하면 모든 비디오를 찾아 반환합니다.

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

export const trending = async (req, res) => {
  const videos = await Video.find({});
  console.log(videos); // [] 반환
  return res.render("home", {
    pageTitle: "Home",
    videos,
  });
};

데이터베이스 서버에서 데이터를 가져오는 작업은 시간이 필요하므로 비동기 처리를 위해 async await 를 사용합니다. 데이터를 모두 가져올 때까지 기다릴 것입니다. Video.find({}) 의 결과를 콘솔에 찍어보면 정상적으로 빈 배열을 반환하는 것을 확인할 수 있습니다.

이전에 home.pug 파일을 만들 때 비디오 객체의 정보를 화면에 보여주되, 만약 빈 배열이면 Sorry를 출력하도록 했습니다. 홈 페이지를 보면 Sorry가 잘 들어가 있습니다.

비디오 업로드 기능 구현

업로드 폼 수정

이제 데이터베이스에 들어갈 동영상을 업로드하는 기능을 실제 데이터베이스와 연동해보겠습니다. 우선 설계한 비디오 스키마대로 업로드 폼을 수정합니다.

extends base

block content
    form(method="POST")
        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")

비디오 스키마에서 사용자에게 직접 받는 데이터는 title, description, hashtags입니다. 이것들을 적을 수 있는 input 을 만들어줍니다. 해쉬태그의 경우 각 태그를 쉼표로 구분하도록 placeholder 에 명시해줍니다.

비디오 document 생성

이제 videoContoller.jspostUpload 컨트롤러에서 사용자 입력값을 가져오겠습니다.

export const postUpload = (req, res) => {
  const { title, description, hashtags } = req.body;
  console.log(title, description, hashtags);
  return res.redirect("/");
};

리퀘스트 바디에서 정상적으로 값을 가져왔는지 콘솔에 찍어 확인해봅니다. 출력값이 잘 나온다면 이제 비디오 스키마의 나머지 부분을 만듭니다.

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

title, description 부분은 사용자의 입력값을 그대로 사용합니다. hashtags 의 경우 input 에서 각 태그를 쉼표로 구분하라고 명시했습니다. 따라서,

  1. .split(",") 으로 쉼표를 기준으로 각 태그들을 분할해 리스트에 저장한 다음
  2. .map((tag) => `#${tag}`) 을 통해 각 태그 앞에 해쉬마크(#)를 달아줍니다.

2번의 경우 사용자가 미리 해쉬태그를 달아서 업로드할 경우도 대비해야 합니다만, 입력값 검사는 추후 구현하도록 하겠습니다.

createdAt에는 자바스크립트의 Date.now() 를 이용해 시각 정보를 저장합니다. meta 속에 들어있는 viewsrating 은 초기값으로 0을 설정해줍니다. 비디오 데이터 생성을 완료하고 console.log 로 확인해봅니다.

{
  title: 'First video',
  description: 'This is First video',
  hashtags: [ '#first', '#video' ],
  createdAt: 1627396487109,
  meta: { views: 0, rating: 0 }
}

입력값이 스키마 구조에 담겨 잘 출력된 것을 볼 수 있습니다.

document 저장

이제 스키마 구조대로 만든 video 도큐멘트를 DB에 저장하겠습니다. 위에서 만든 비디오 데이터 객체를 Video.create() 에 인자로 전달합니다. 데이터베이스 저장을 마치고 redirect 가 일어나야 하므로 컨트롤러를 async 함수로 만들고 await를 사용하여 저장을 기다려줍니다.

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

이제 업로드 페이지에서 폼을 작성하고 업로드 버튼을 누르면 비디오 데이터가 DB에 실제로 저장됩니다. 폼을 작성하고 업로드 버튼을 누르면 비디오 데이터가 정상적으로 홈 화면에 불러와진 것을 확인할 수 있습니다.

또한 터미널에서 $ mongo 명령어로 mongo shell에 진입한 후 show dbs 명령을 입력하면 정상적으로 wetube 항목이 생긴 것을 볼 수 있습니다.

> show dbs
admin    0.000GB
config   0.000GB
local    0.000GB
wetube   0.000GB

이번 포스팅에서는 MongoDB와 mongoose의 기초에 대해 배웠고, 아직은 텍스트 뿐이지만 실제로 비디오 데이터를 데이터베이스에 저장하고 불러와보았습니다. 다음 포스팅에서도 서버와 데이터베이스와의 연동에 대해 계속해서 공부해보겠습니다.


<참고 문서>

profile
프론트엔드 웹 개발자를 목표로 하고 있습니다.

0개의 댓글