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


2021년 7월 22일

지난 포스팅에서 기초 셋업과 Node.js의 프레임워크인 Express의 기초에 대해 공부했습니다. 이어서 Express의 router 개념에 대해 알아보겠습니다.

지금까지 서버에서 받은 요청은 루트 / 하나 뿐입니다.

const handleHello = (req, res) => {
  res.send("Hello!");
};

app.get("/", handleHello);

그런데도 코드가 차지하는 공간은 제법 있어 보입니다. 아마 컨트롤러의 덩치가 훨씬 커진다면 코드는 더욱 복잡해질 것입니다.

웹 서비스가 만들어짐에 따라 수십에서 수백 개의 경로가 생긴다면 코드는 훨씬 복잡해질 것입니다. 따라서 URL을 종류별로 묶어놓을 수 있는 장치가 필요합니다. 이 이야기는 밑으로 내려가면서 계속 이어가겠습니다.

도메인 설계

우선 사용자 입장에서 도메인을 설계해봅시다. 유튜브의 도메인은 크게 두 가지로 분리할 수 있습니다. videouser입니다.

시청, 업로드, 삭제 등의 이벤트가 일어나는 video와 로그인, 프로필 등을 관리하는 user가 있습니다. 각각의 상황에 맞는 페이지를 만들어나가면 다음과 같을 것입니다.

video:

  • /trending-videos
  • /see-video
  • /edit-video
  • /search-video
  • /upload-video
  • /delete-video

user:

  • /join
  • /login
  • /edit-user
  • /remove-user
  • /logout-user
  • /see-user

이 경로들에 대한 요청을 모두 app.get()으로 받고, 각각에 해당하는 컨트롤러를 만들고, 필요한 미들웨어까지 만든다면 server.js 파일은 필요 이상으로 복잡해집니다.

문제는 URL에도 있습니다. URL이 조직화되어있지 않기 때문에 /upload-video, /delete-video 처럼 video라는 문자열을 계속 반복해야 합니다.

router

Express의 router는 마치 상위 디렉터리 안에 하위 디렉터리들이 위치하는 것처럼 URL을 조직적으로 관리할 수 있게 해줍니다.

만약 /videos/delete 요청이 들어오면 해당 요청은 /videos를 담당하는 라우터에게 보내집니다. 그리고 비디오 라우터가 /delete를 담당하는 컨트롤러를 통해 요청에 응답하게 됩니다.

const globalRouter = express.Router();
const videoRouter = express.Router();
const userRouter = express.Router();

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

이 프로젝트에서는 총 3개의 라우터를 사용합니다. 유저와 관련된 userRouter, 동영상과 관련된 videoRouter, 그리고 메인 페이지를 담당하는 globalRouter입니다. 각각 express.Router()로 라우터를 생성합니다. 그리고 app.use()에 라우터가 담당하는 경로와 함께 라우터를 인자로 줍니다.

각각의 라우터가 담당하는 경로를 할당하는 방법은 이전과 똑같습니다.

const watch = (req, res) => {
  res.send("Watch videos!");
};

videoRouter.get("/watch", watch);

지금까지 express appget()으로 요청을 받을 수 있게 해줬던 것처럼, 라우터에도 get()을 통해 특정 경로의 요청을 받게 할 수 있습니다. videoRouter에게 /watch 요청을 받을 수 있게 해줬습니다. 이제 사용자가 /videos/watch를 요청하면 videoRouter에서 처리할 것입니다.

여기서 볼 수 있듯이 각각의 라우터는 마치 하나의 '미니 어플리케이션'처럼 기능합니다. videoRouter/watch, /edit 등의 요청을 처리할 수 있는 작은 어플리케이션인 셈입니다.

파일 분할

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

const app = express();

const PORT = 4000;

const globalRouter = express.Router();
const videoRouter = express.Router();
const userRouter = express.Router();

const watch = (req, res) => {
  res.send("Watch videos!");
};

videoRouter.get("/watch", watch);

app.use(morgan("dev"));
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);

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

Express 틀을 만들고, 라우터를 생성하고, 임시로 videoRouter에게 /watch 컨트롤러를 달아주었습니다. 라우터를 만들어 경로별 요청을 분류한 것까지는 좋았는데, server.js 파일이 복잡해졌습니다. 도메인 설계에서 구상한 모든 페이지를 만든다면 라우터와 컨트롤러 등이 섞여 보기에 좋지 않을 것입니다.

이제 라우터는 라우터끼리, 컨트롤러는 컨트롤러끼리 모여있을 수 있도록 프로젝트 디렉토리를 재구성해보겠습니다.

  1. src 폴더 안에 routerscontrollers 폴더를 생성합니다.

  2. routers 폴더 안에 globalRouter.js, videoRouter.js, userRouter.js 파일을 만듭니다.

  3. controllers 폴더 안에 videoController.js, userController.js 파일을 만듭니다.

    controllers 폴더 안에 globalController.js를 만들지 않는 이유는 홈 화면에 표시될 컨텐츠의 종류도 결국 유저 아니면 비디오와 관련된 것이기 때문입니다.

이제 server.js에 적었던 라우터 관련 코드를 각각의 파일로 옮기겠습니다. globalRouter를 예시로 설명하겠습니다.

import express from "express";

const globalRouter = express.Router();

export default globalRouter;

라우터도 Express의 기능이므로 express를 불러옵니다. 그리고 globalRouter를 생성한 뒤 export 하여 server.js에서 불러올 수 있도록 합니다.

videoRouter, userRouter에도 같은 작업을 반복합니다.

그리고 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";

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

app.use(morgan("dev"));
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);

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

파일이 훨씬 깔끔해졌습니다.

라우터 설계

이제 파일 정리를 마치고 각각의 라우터 구조를 설계해보겠습니다. 현재 시점에서는 대략적인 방향만 잡아놓고 앞으로 수정해나갈 계획입니다.

globalRouter

globalRouter는 메인 화면과 관련된 페이지들을 모아놓은 라우터입니다. 다음과 같이 구성할 수 있을 것입니다.

globalRouter.get("/", trending);
globalRouter.get("/join", join);
globalRouter.get("/login", login);
globalRouter.get("/search", search);

사용자가 메인 페이지에 들어오면 유튜브는 인기동영상 리스트를 보여줍니다. 이는 trending 컨트롤러가 담당합니다. 그리고 회원가입, 로그인, 검색 등의 기능이 메인 페이지에 들어갈 수 있습니다.

videoRouter

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

비디오와 관련해서는 업로드, 시청, 수정, 삭제 등의 기능이 있을 것입니다.

여기서 /:id 라고 적혀 있는 부분은 URL 파라미터입니다. 유튜브의 모든 동영상은 동영상을 시청하고 댓글을 남길 수 있는 페이지를 가지고 있지만, 그렇다고 해서 구글이 모든 동영상을 위한 페이지를 직접 제작해주는 것은 아닙니다.

/:id에는 해당 동영상의 id값이 들어가게 되고, 미리 만들어진 탬플릿 페이지에 해당 동영상과 관련된 모든 데이터가 채워집니다. 이 부분은 데이터베이스 파트에서 자세히 다룰 예정입니다.

userRouter

userRouter.get("/logout", logout);
userRouter.get("/edit", edit);
userRouter.get("/remove", remove);
userRouter.get("/:id", see);

유저 관련 기능에는 로그아웃, 유저 프로필 수정, 회원탈퇴, 그리고 프로필 보기가 있습니다. video와 마찬가지로 유저 정보를 데이터베이스에서 불러올 때 더 자세히 다루겠습니다.

controller 연결

각각의 라우터를 설계할 때 URL과 함께 해당 요청을 처리하는 컨트롤러 함수를 함께 전달했습니다. 이제 임시로 컨트롤러를 만들어보겠습니다.

userController

export const join = (req, res) => res.send("Join");
export const login = (req, res) => res.send("Login");
export const edit = (req, res) => res.send("Edit user");
export const remove = (req, res) => res.send("Remove user");
export const logout = (req, res) => res.send("Logout user");
export const see = (req, res) => res.send("See user");

모든 컨트롤러는 라우터에서 쓸 수 있게끔 export 해줍니다. 여기서 joinloginglobalRouter가, 나머지 컨트롤러는 userRouter가 사용합니다.

videoController

export const trending = (req, res) => res.send("trending");
export const see = (req, res) => res.send("watch");
export const edit = (req, res) => res.send("edit");
export const search = (req, res) => res.send("Search");
export const upload = (req, res) => res.send("upload");
export const deleteVideo = (req, res) => res.send("deleteVideo");

역시 모든 컨트롤러를 export 했습니다. trendingglobalRouter가 가져다 쓸 것입니다. 나머지 컨트롤러는 videoRouter가 사용합니다.

라우터에서 import 하기

이제 controllers 폴더에 만들어놓은 컨트롤러들을 각각의 라우터가 import 해서 사용해야 합니다.

import express from "express";
import { trending, search } from "../controllers/videoContoller";
import { join, login } from "../controllers/userContoller";

const globalRouter = express.Router();

globalRouter.get("/", trending);
globalRouter.get("/join", join);
globalRouter.get("/login", login);
globalRouter.get("/search", search);

export default globalRouter;

globalRouter에서 사용하는 컨트롤러를 다음과 같이 import합니다. 다른 컨트롤러도 마찬가지입니다.

import express from "express";
import { logout, edit, remove, see } from "../controllers/userContoller";

const userRouter = express.Router();

userRouter.get("/logout", logout);
userRouter.get("/edit", edit);
userRouter.get("/remove", remove);
userRouter.get("/:id", see);

export default userRouter;
import express from "express";
import { upload, see, edit, deleteVideo } from "../controllers/videoContoller";

const videoRouter = express.Router();

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

export default videoRouter;

이제 이번 프로젝트의 라우터 설계의 윤곽이 어느정도 잡혔습니다. 앞으로 데이터베이스를 연동하고, HTML을 렌더링하는 과정을 거치면서 이 부분은 계속 수정될 것입니다.

다음 포스팅에서는 pug를 사용해 서버에서 HTML을 효율적으로 렌더링하는 방법에 대해 알아보겠습니다.


<참고 문서>

좋은 웹페이지 즐겨찾기