
효율적인 REST API 서버를 구축을 위해 폴더 구조를 명확하게 잡아야 한다. 명확한 프로젝트 아키텍처는 개발자의 목적에 맞는 코드에 빠르게 찾아갈 수 있고, 다른 사람이 볼 때도 전체적인 구조를 알아보기가 쉬워진다.
현재, 잘 알려져 있고 나도 사용 중인 NodeJs(express) 프로젝트 아키텍처를 정리해 본다. 템플릿 또한 깃허브에 저장해 놓았다.
다음과 같이 코드들이 관심사가 분리되어 원하는 파일을 찾는 것이 쉬워졌다. 프로젝트 아키텍처에는 완벽한 것이 없기 때문에, 본인에게 필요한 폴더가 있다면 추가해도 상관 없다.
src
├── api # api route
├── config # environment variable
├── loaders # init module like express, mongodb connection
├── models # database model(types)
├── services # business logic
└── app.ts # app entry point
서버의 시작점이다. express를 초기화하고, loader를 실행하고 서버를 작동하는 코드 등이 작성되어 있다.
//app.ts
import express from "express";
import loader from "../src/loaders";
import config from "../src/config";
async function server() {
const app = express();
await loader({ expressApp: app });
const server = app.listen(config.port, () => {
console.log(`http://localhost:${config.port}`);
});
}
다양한 모듈의 목적 파일이 들어있다. express의 추가적인 설정이나 몽고디비의 커넥션 등의 작업을 분리 시키는 것이다.
로더를 사용하여 진입점 파일의 가독성이 좋아지고, 테스트가 편리해졌다.
//app.ts
import express from "express";
import loader from "../src/loaders";
import config from "../src/config";
async function server() {
const app = express();
await loader({ expressApp: app });
const server = app.listen(config.port, () => {
console.log(`http://localhost:${config.port}`);
});
}
// loader/index.ts
import { Application } from "express";
import expressLoader from "./express";
export default async ({ expressApp }: { expressApp: Application }) => {
await expressLoader({ app: expressApp });
};
// loader/express.ts
import express, { Application } from "express";
import cors from "cors";
import routes from "../api";
export default ({ app }: { app: Application }) => {
app.use(cors());
app.use(express.json());
app.use("/api", routes());
};
route폴더와 middleware폴더로 나뉘어 진다.
route: REST API의 경로를 포함한다.middleware: 요청마다 인증 절차를 거치는 등의 다양한 미들웨어 함수들을 포함한다.// loader/express.ts
import routes from "../api";
export default ({ app }: { app: Application }) => {
app.use("/api", routes());
};
// api/index.ts
import { Router } from 'express';
import auth from './routes/auth';
export default () => {
const app = Router();
auth(app);
return app
}
// api/route/auth/index.ts
import { Router } from "express";
const route = Router();
export default (app: Router) => {
app.use("/account", route);
route.post("/signin", signin);
};
비즈니스 로직들이 들어있는 폴더이다. 로직들은 분명한 목적을 갖고 있으며, 단일 책임 원칙에 맞게 코드를 작성하는 것이 좋다.
services 계층을 사용하면, 컨트롤러를 사용할 때, 코드의 흐름이 읽기 쉬워져 어떤 역할을 하는지 파악하기에도 용이하다.
service계층 주의사항
01. req, res를 전달하지 않는다.
02. 컨트롤러에 비즈니스 로직을 작성하지 않는다.
03. HTTP계층과 관련된 것들은 하지 않는다. (상태 코드, header 사용 등)
다음은 오류가 있는 코드 예제이다.
// auth 컨트롤러
route.post("/auth", tokenAuth);
// ❌ => 3가지 모두 문제가 있는 코드
export const tokenAuth = async (req: Request, res: Response): Promise<void> => {
try {
const authorizationHeader = req.headers["authorization"];
if (!authorizationHeader) return;
const accessToken: string = authorizationHeader.split(" ")[1];
const data = jwt.verify(accessToken, config.jwtAccessKey as string);
const user = toUserFormat(data);
res.status(200).send(user);
} catch (error) {
console.log(
"access Token 만료: ",
(error as jwt.TokenExpiredError).expiredAt
);
res.send("expired");
}
};
services계층의 원칙에 맞게 비즈니스 로직을 컨트롤러와 분리하여 작성한다.
// ✅ => 올바른 방식
route.post("/auth", (req: Request, res: Response) => {
const authorizationHeader = req.headers["authorization"];
if (!authorizationHeader) return;
const user = await tokenAuth(authorizationHeader)
res.send(user);
})
데이터베이스의 모델 스키마 구조 등이 들어간다.
import mongoose from 'mongoose';
const User = new mongoose.Schema(
{
name: {
type: String,
...
},
age: {
type: String,
...
},
},
{ timestamps: true },
);
다양한 환경 변수 설정들이 들어있는 폴더이다. .env는 프로젝트 최상위 경로에 존재하며 환경 변수 값들을 config에서 관리하는 것이다.
config파일을 통해 환경 변수를 가져다 쓸 경우 코드의 길이가 짧아졌고, 어디서 사용하는지 코드 추적이 가능해졌다.
// config/index.ts
import dotenv from "dotenv";
const envFound = dotenv.config();
if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
}
export default {
port: process.env.PORT,
mognodbURL: process.env.MONGODB_URL,
mongodbPassword: process.env.MONGODB_ADMIN_PASSWORD,
jwtAccessKey: process.env.JWT_ACCESS_KEY,
jwtRefreshKey: process.env.JWT_REFRESH_KEY,
};
나의 메인은 프론트엔드이기 때문에 nodeJs 백엔드를 썩 잘 다루지 못한다. 그렇지만 깃허브 예제를 잘 따라간다면 나름 괜찮은 express 프로젝트를 구축할 수 있을 거라 생각하여 여러가지 배우면 좋을 라이브러리를 정리해 본다.
typedi
logger
agenda
celebrate / joi
winston
NodeJs(express)를 사용한다면 아키텍처가 상당히 자유롭다. 그래서 몇몇의 사람들이 나처럼 아키텍처를 갖고 갈팡질팡 하는 일이 많을 것이라고 생각한다. 이를 위해 NestJs 프레임워크가 존재한다. 이를 공부하는 것도 나쁘지 않아 보인다.