Node.js/Express Routing/모듈화/프로젝트 구조
03 May 2021☄ What is Routing
애플리케이션 엔드 포인트(URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식
라우팅은 express
어플리케이션 인스턴스로부터 파생된 라우트 메소드들을 이용하며, 이 route 메소드들은 클라이언트로부터 http 메소드들에 대한 요청이 특정 URL로 오면 그 요청에 대한 적절한 처리와 응답을 통해 이루어 진다.
기본적인 GET
및 POST
메소드에 대한 라우팅
// 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/edit
를 userRouter
에 /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으로 불러와야 한다.