on1ystar 머릿속을 정리, 기록하는 곳

Node.js/Express Routing/모듈화/프로젝트 구조



의문점이나 지적 등의 관심 및 조언을 위한 댓글이나 메일은 언제나 환영이고 감사합니다.


☄ What is Routing


애플리케이션 엔드 포인트(URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식

라우팅은 express 어플리케이션 인스턴스로부터 파생된 라우트 메소드들을 이용하며, 이 route 메소드들은 클라이언트로부터 http 메소드들에 대한 요청이 특정 URL로 오면 그 요청에 대한 적절한 처리와 응답을 통해 이루어 진다.

기본적인 GETPOST 메소드에 대한 라우팅

// GET method route
app.get("/", function (req, res) {
  res.send("GET request to the homepage");
});

// POST method route
app.post("/", function (req, res) {
  res.send("POST request to the homepage");
});


☄ 도메인 별 Routing


라우터들을 나눌 때(url을 만들 때) 기능을 사용하는, 혹은 사용되는 데이터를 기준으로 도메인을 나누는 것이 중요하다.

예를 들어 아래와 같은 라우터들을 만들어야 할 때,

  • home
  • 회원가입
  • 로그인
  • 검색
  • 프로필 수정
  • 유저 삭제
  • 비디오 시청
  • 비디오 수정
  • 비디오 삭제
  • 비디오 댓글
  • 비디오 댓글 삭제

크게 3~4개의 도메인으로 나눌 수 있다.

global (루트 주소에서 바로) → /

  • home → /
  • 회원가입 → /join
  • 로그인 → /login
  • 검색 → /search

user 데이터를 기준으로 → /users

  • 프로필 수정 → /users/edit
  • 유저 삭제 → /users/delete

video 데이터를 기준으로 → /videos

  • 비디오 시청 → /videos/watch
  • 비디오 수정 → /videos/edit
  • 비디오 삭제 → /videos/delete
  • 비디오 댓글 → /videos/comments
  • 비디오 댓글 삭제 → /videos/comments/delete

video 관리 → /videos

  • 비디오 시청 → /videos/watch
  • 비디오 수정 → /videos/edit
  • 비디오 삭제 → /videos/delete

Video 댓글 관리 → /videos/comments

  • 비디오 댓글 → /videos/comments
  • 비디오 댓글 삭제 → /videos/comments/delete


☄ express.Router()


express.Router 클래스를 사용하면 모듈식으로 라우트들을 쌓아가며 핸들러를 작성할 수 있다.

Router 인스턴스는 완전한 미들웨어이자 라우팅 시스템이며, 따라서 “미니 앱(mini-application)”이라고 불리는 경우가 많다. 미들웨어기 때문에 use() 메소드의 argument로 사용할 수 있다.

또한 Router 인스턴스는 express.Application의 라우트 메소드(get(), post(), put(), …) 기능을 그대로 제공한다.

위의 라우트들을 예시로 사용해 보면 다음과 같다.

// ...

const globalRouter = express.Router();
globalRouter.get("/", (req, res) => res.send("HOME !"));
const userRouter = express.Router();
userRouter.get("/edit", (req, res) => res.send("EDIT USER !"));
const videoRouter = express.Router();
videoRouter.get("/watch", (req, res) => res.send("WATCH VIDEO !"));

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

// ...

각각 라우터들은 use()로 지정한 라우트를 base로 하여 라우트 메소드에 매칭되는 url의 요청들을 처리한다.

  • globalRouter → base: /
  • userRouter → base: /users
  • videoRouter → base: /videos


☄ Clean Code를 위한 프로젝트 구조


위와 같이 라우터들과 컨트롤러들을 라우트 메소드와 함께 서버가 도는 server.js에 다 때려 박아버리는 건 보기에도 지저분하고, 코드가 길어지면서 유지보수 비용이 말도안되게 커진다.

따라서 좋은 라우팅을 하기 위해서는 라우터와 컨트롤러들의 디렉토리를 나눠서 설계할 필요가 있다.

이때, 중요한 점은 코드들을 파일별로 나눴을 때 서로 연동시키는 방법이다. express는 모든 파일들이 모듈화 되어 있다. 따라서 다른 파일의 코드를 모듈처럼 가져다 쓰기 위해서는 import 시켜서 사용하면 되는데, 또 한 가지 중요한 부분이 import 시킬 파일에서 어떤 코드(내용)을 export 시킬 지 작성해 줘야 한다. 우리가 사용하는 node_modules 안의 모듈들도 모두 export한 내용을 import로 가져다 쓰고 있는 것이다.

routers 분리

/* 디렉토리 구조 
├─┬─routers
│ ├───globalRouter.js
│ ├───userRouter.js
│ └───videoRouter.js
├───server.js
*/
// globalRouter.js
import express from "express";

const globalRouter = express.Router();

globalRouter.get("/", (req, res) => res.send("HOME !"));

export default globalRouter;
// userRouter.js
import express from "express";

const userRouter = express.Router();

userRouter.get("/edit", (req, res) => res.send("EDIT USER !"));

export default userRouter;
// videoRouter.js
import express from "express";

const videoRouter = express.Router();

videoRouter.get("/watch", (req, res) => res.send("WATCH VIDEO !"));

export default videoRouter;

이렇게 함으로써 각 파일이 isolated 상태가 되어 /users/edituserRouter/edit로 작성할 수 있어 구조적으로나 의미적으로나 더 깔끔하고 효율적이다. 이렇게 작성할 수 있는 이유는 다시 한 번 강조하면 express.Router() 덕분이다.

이제 라우터 파일에서 컨트롤러들을 분리해야 한다. 위의 예제에서는 라우팅을 처리할 경로가 각각 1개 씩 밖에 없고, 내용도 간단하게 한 줄로만 작성해 인라인 함수로 작성했다.

하지만 점점 처리할 url이 많아지고, 컨트롤러에서는 실제로 많은 서비스들이 처리되므로 코드 양도 방대해 질 수 밖에 없다. 따라서 편의를 위해 경로를 설정해주는 라우터핵심 비지니스 로직이 작성되는 컨트롤러들을 따로 분리시킬 필요가 있다.

controllers 분리

/* 디렉토리 구조 
├─┬─controllers
│ ├───userController.js
│ └───videoController.js
├─┬─routers
│ ├───globalRouter.js
│ ├───userRouter.js
│ └───videoRouter.js
├───server.js
*/

이때 고려해야 할 점이 있다. 위 라우팅에서는 url 구조의 깔끔함, 편의를 위해 global. user, video 3개의 라우터로 도메인을 나눴다. 하지만 컨트롤러로 나눌 때는 그 행위, 서비스를 하는 주체 혹은 서비스가 되는 대상이 무엇인 지를 잘 생각해야 한다.

예를 들어 위 globalRouter에서 /을 처리할 컨트롤러는 user일까? video일까? 그건 이 프로젝트가 어떤 서비스를 하는 웹, 어플리케이션인 지에 따라 달라질 수 있다.

지금 만들고 있는 것은 Youtube를 클론 코딩하는 것이기 때문에 home을 나타내는 /에서는 video들이 나열되야 하므로 videoController에 작성하는 편이 좋다.

// globalRouter.js
import express from "express";
import { join } from "../controllers/userController";
import { trending } from "../controllers/videoController";

const globalRouter = express.Router();

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

export default globalRouter;
// userController.js
export const join = (req, res) => res.send("Join");
export const edit = (req, res) => res.send("Edit user");
export const remove = (req, res) => res.send("Remove user");
// videoController.js
export const trending = (req, res) =>
  res.send("This is home page for videos !");
export const watch = (req, res) => res.send("Watch video !");
export const edit = (req, res) => res.send("Edit video !");

만약 위 컨트롤러들을 한 곳에 모아놨다면, usersEdit, videosEdit과 같이 함수 명을 구분해서 작성해야 하지만 폴더 구조로 이미 나뉘어 져 있기 때문에 가독성이나 효울성 측면에서도 훨씬 좋다.

// globalRouter.js
import express from "express";
import { join } from "../controllers/userController";
import { trending } from "../controllers/videoController";

const globalRouter = express.Router();

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

export default globalRouter;

/라우트의 컨트롤러를 굳이 home이라 하지 않고, 의미적으로 video들의 트렌드들을 보여주는 곳이 홈이므로 trending이라는 name을 줬다.

// userRouter.js
import express from "express";
import { edit, remove } from "../controllers/userController";

const userRouter = express.Router();

userRouter.get("/edit", edit);
userRouter.get("/remove", remove);

export default userRouter;
// videoRouter.js
import express from "express";
import { edit } from "../controllers/userController";
import { watch } from "../controllers/videoController";

const videoRouter = express.Router();

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

export default videoRouter;

export

위 코드를 보면 다른 파일에서 모듈처럼 사용하기 위해 라우터나 컨트롤러를 export하는 방법이 2가지다.

  • export default

그 파일(모듈)에서 1개의 객체만을 내보내겠다는 의미다. 모듈화를 잘했을 때 사용하기 좋은 방식이다. import할 때 가져올 대상이 1개로 명확해, 원하는 name을 지정해 줄 수 있다.

  • export

파일(모듈)안에 복수의 함수, 객체가 있어도 각각을 모두 내보낼 수 있다. import할 때 복수의 대상을 가져올 수 있으므로 항상 {}로 묶어줘야 하며, 그 파일(모듈)에서 선언 시 지정한 name으로 불러와야 한다.

Reference

https://nomadcoders.co/wetube

https://expressjs.com/ko/guide/routing.html