본문으로 바로가기

cineps를 만든 후 복기 겸 더 나은 node.js 아키텍쳐를 위해서 정리해보았습니다.

 

 

견고한 node.js 프로젝트 설계하기

본 글은 Sam Quinn의 “Bulletproof node.js project architecture” 글을 번역한 것입니다. [Bulletproof node.js project architecture 🛡️ Express.js is great frameworks f

velog.io

원본 : softwareontheroad.com/ideal-nodejs-project-structure/

 

특정한 사람만이 사용하는 아키텍쳐가 아닌 node.js 기반의 서버에서는 이런 아키텍쳐를 많이 사용합니다.

 

위와 같은 아키텍쳐를 프레임워크로 고정시킨 것이 Nest.js입니다.

Nest.js를 사용하다보면, 위에서 언급한 내용들이 강제되는 것을 확인해보실 수 있습니다.

 

제가 지금까지 node를 사용해오던 방법은 다음과 같았습니다.

src
│   app.js          # App entry point
└───routes          # 라우팅
└───controllers     # 컨트롤러 (주요 로직들)
└───models          # Database models
└───middleware      # 미들웨어
└───utils           # 각종 유틸 함수들
└───types           # Type declaration files (d.ts) for Typescript
└───dist            # 타입스크립트 컴파일 후 배포 폴더

 

그런데 위 포스팅에서는 다음과 같은 파일 구조를 잡고 진행하더군요. 왜 이렇게 진행했는지 하나씩 알아봅시다.

src
│   app.js          # App entry point
└───api             # Express route controllers for all the endpoints of the app
└───config          # Environment variables and configuration related stuff
└───jobs            # Jobs definitions for agenda.js
└───loaders         # Split the startup process into modules
└───models          # Database models
└───services        # All the business logic is here
└───subscribers     # Event handlers for async task
└───types           # Type declaration files (d.ts) for Typescript

 

 

1. 비즈니스 로직은 controller가 아니라 service 계층에 넣자.

 

예전에 제가 사용하던 부분의 일부를 가져오자면, routes에서 경로 잡고, controller에서 로직을 작성하는 방식이죠.

// routes
apiRouter.post("/registerView", postRegisterView);

// controller
export const postRegisterView = async (req, res) => {
  ... 중략
};

 

나중에 머리가 굵어지면서 이런 분리가 번거로워졌고, 다음과 같이 합쳐서 작성했습니다.

그런데 이런 방법은 지금 당장은 편하더라고, 로직이 복잡해질수록 코드가 커졌고, 결국 한 경로당 처리하는 로직의 줄이 500줄이 넘어가는 기염을 토하기도 했습니다...

apiRouter.post("/registerView", (req, res) => {
  ... 미들웨어 떡칠....
  
  ... 비즈니스 로직...
  
  ... 전체를 try/catch/finally로 감싸고...
  
  ... 경우에 따라 적절하게 return값을 정해주자...
});

 

Nest.js에서도 사용하였듯 서비스단을 별도로 분리하여 controller에서는 비즈니스 로직을 실행만하고, 특정 값을 반환하는 데에만 집중하도록합시다. 무슨 말인지 아실거라 믿습니다 ㅎ

apiRouter.post("/registerView", (req, res) => {
  ... 미들웨어도 모듈화 하여 분리해놓는게 좋다.
  
  UserService.registerView(req.body) // 로직
  
  return res.json({...}) // 반환
});

 

2. 깔끔한 엔트리 포인트 관리와 가독성을 위해 loader를 쓰자.

 

프론트에서 요청 전까지 보여주는 로더가 아니라, 백엔드에서 가독성을 위해 만든 loader를 말합니다.

주로, 엔트리 포인트인 app.js에서는 다음과 같은 꼴의 코드가 나오게 됩니다.

 

아무리 주석을 달았다고는 하지만 숨이 좀 막히죠...

const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const morgan = require("morgan");
const cors = require("cors");
const app = express();
const dotenv = require("dotenv").config();
const helmet = require("helmet");
const hpp = require("hpp");

const PORT = process.env.PORT || 5000;
const NODE_ENV = process.env.NODE_ENV;

// database
const mongoose = require("mongoose");
const dbAddress =
  NODE_ENV === "production"
    ? process.env.PROD_MONGO
    : "mongodb://localhost:27017";

mongoose
  .connect(dbAddress, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: true,
    useFindAndModify: false,
    dbName: "KinoProject",
  })
  .then(() => console.log(`mongoDB connected`))
  .catch((err) => console.error(err));

// middleware
if (NODE_ENV === "production") {
  console.log("프로덕션 환경입니다.");
  app.use(hpp());
  app.use(helmet());
  app.disable("x-powered-by"); // x-powered-by 헤더를 제거하여 express임을 숨기자

 ... 중략
}

... 여러 미들웨어들...
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: false }));
app.use(express.static(path.join(__dirname, "..", "front", "build")));
app.use(
  cookieParser(process.env.COOKIE_SECRET, { sameSite: "none", secure: true })
);
... 중략

//routes
... 많은 라우트들...
const apiRouter = require("./routes/api");
const oAuthRouter = require("./routes/oauth");
const userProfileRouter = require("./routes/userprofile");
const adminRouter = require("./routes/admin");.
const WrapRoute = "server";
app.use(`/${WrapRoute}/api`, apiRouter);
app.use(`/${WrapRoute}/oauth`, oAuthRouter);
app.use(`/${WrapRoute}/userprofile`, userProfileRouter);
app.use(`/${WrapRoute}/admin`, adminRouter);

app.use(function (err, req, res, next) {
  console.error(err.message);
  res
    .status(500)
    .send({ status: 500, message: "internal error", type: "internal" });
});

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

 

로더를 이용하여 엔트리 포인트를 정리하자면 다음과 같이 될 수 있다.

async function startServer() {
  const app = express();

  // 로더를 이용해서 미들웨어, 라우팅, DB 연결 등을 모두 만들어 줬다.
  await loaders.init({ expressApp: app });

  // 여기서는 express app을 리슨만하기로 하자.
  app.listen(process.env.PORT, err => {
    if (err) {
      console.log(err);
      return;
    }
    console.log(`Your server is ready !`);
  });
}

startServer();

 

로더는 별게 아니고, 아래처럼 적당히 필요한 부분을 나눠주기만 하면 된다. 깰끔!

import expressLoader from './express';
import mongooseLoader from './mongoose';

export default async ({ expressApp }) => {
  const mongoConnection = await mongooseLoader();
  console.log('MongoDB Intialized');
  await expressLoader({ app: expressApp });
  console.log('Express Intialized');

  // ... more loaders can be here

  // ... Initialize agenda
  // ... or Redis, or whatever you want
}

 

 

구체적으로 연결하는 각 로더들은 각자 만들어 async/await로 동작하게 만들어주면 된다.

import * as mongoose from 'mongoose'
export default async (): Promise<any> => {
  const connection = await mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });
  return connection.connection.db;
}

 

 

3. 의존성 주입(DI)

 

... 작성

 

4. pub/sub 모델

 

... 작성

 

 

 

==== tips

 

 

1. 협업을 위해서 .prettierrc도 하고, .editorconfig도 하자.

{
    "printWidth": 150,
    "tabWidth": 2,
    "singleQuote": true,
    "trailingComma": "all",
    "semi": true,
    "arrowParens": "avoid"
}
# .editorconfig
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

 

 

2. .huskyrc를 이용해서 master push를 막는 훅을 달아놓거나 lint를 돌리는 등의 필요한 작업을 세팅해놓자.

changhoi.github.io/posts/etc/husky/

library.gabia.com/contents/8492/

{
  "hooks": {
    "pre-commit": "npm run lint"
  }
}

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체