nest.js를 블랙박스처럼 사용하는 것보다, 좋은 아키텍쳐 선례를 찾아 스스로 구현해보고 그 이후에 Nest.js를 이용하는 것이 좋다고 판단하여서 프로젝트 코드 중 nest를 전부 삭제했다.
추가로, 과거 graphql을 포함한 보일러 플레이트를 만든 적이 있는데, 그것보다 좀 더 발전된 형태로 만들어보고 싶어서 만들어보았다.
prequisition
node.js에 대한 기초적인 상식들 : darrengwon.tistory.com/192
전에 만든 type-node-graphql boilerplate : darrengwon.tistory.com/887?category=921119
node.js 아키텍쳐 : softwareontheroad.com/ideal-nodejs-project-structure/(번역)
typescript 컴파일 세팅, controller와 service의 분리 등을 알고 시작합시다.
추가로, 구성은 typescript-node-starter 보일러 플레이트를 참고하였습니다.
www.npmjs.com/package/typescript-express-starter
Contents
1. 환경 변수 validation
envalid 154kb, joy 515kB다. envalid를 사용해보기로 한다.
사용법은 비교적 간단한 편이니 npm 주소로 갈음한다.
import { cleanEnv, port, str } from 'envalid';
const validateEnv = () => {
cleanEnv(process.env, {
NODE_ENV: str({ choices: ['development', 'test', 'production', 'staging'] }),
PORT: port(),
API_VER: str(),
});
};
export default validateEnv;
NODE_ENV에 고의적으로 port값이 와야 한다고 잘못된 validation 설정을 통해서 에러 메세지를 확인해보았다.
================================
Invalid environment variables:
NODE_ENV: Invalid port input: "development"
Exiting with error code 1
================================
2. api version 명시 겸 리버스 프록시를 위한 wrapRouter
cineps 프로젝트를 작성했을 때 라우트 작성은 아래와 같이 했다.
globalRouter = Router();
globalRouter.get("/", (req, res) => ...);
globalRouter.get("/about", (req, res) => ...);
export default globalRouter
이러한 방식으로 작성했을 때의 문제점은
1. api version 핸들링이 되지 않고, 주먹 구구식으로 하게 된다.
2. 모놀리식한 프로젝트를 작성할 때, 특정 경로로 접속하면 백엔드 포트로 보내는 reverse proxy를 작성하게 되는데 공통적인 경로가 없어 다음과 같은 귀찮은 작성을 해주어야 했다.
const WrapRoute = "server";
app.use(`/${WrapRoute}/api`, apiRouter);
app.use(`/${WrapRoute}/oauth`, oAuthRouter);
app.use(`/${WrapRoute}/userprofile`, userProfileRouter);
app.use(`/${WrapRoute}/admin`, adminRouter);
3. 라우트 코드에서 반복되는 부분이 분명 많았음에도 반복적으로 코드를 작성하고 있어 어딘가 투박한 느낌을 준다.
따라서, route 관리는 다음과 같이 하도록 하였다.
우선 router를 작성한다. 컨트롤러를 분리하였고, 컨트롤러 내부에는 비즈니스 로직을 처리하는 서비스가 존재한다. 이 부분은 nest.js를 사용해보았다면 익숙해졌을 것이라 생각한다.
import { Router } from 'express';
import Route from '../interfaces/route.interface';
import IndexController from '../controllers/index.controller';
class IndexRoute implements Route {
public path = '/';
public router = Router();
public indexController = new IndexController();
constructor() {
this.initializeRoutes();
}
private initializeRoutes() {
// 쉽게 생각하면, router.get('/', controller)이다.
this.router.get(`${this.path}`, this.indexController.index);
// 추가적으로 작성하고 싶은 경우 다음과 같이 하면 된다.
// this.router.get(`${this.path}/:id/whatyouwant`, this.indexController.whatyouwrote);
}
}
이 router를 App에서 받아서 다음과 같이 전체를 감싸주면 된다.
apiVer 상수는 어디에서나 관리해도 괜찮지만 환경 변수로 넣어주기로 하였다.
import express from 'express';
import Route from './interfaces/route.interface';
class App {
public apiVer: string | undefined;
constructor(routes: Route[]) {
this.apiVer = process.env.API_VER;
this.initializeRoutes(routes);
}
public initializeRoutes(routes: Route[]) {
// 전체를 감싸줌으로서 /v1/... 꼴로 감싸주게 된다.
routes.forEach(route => {
this.app.use(`/${this.apiVer}`, route.router);
});
}
}
export default App;
결과적으로
/v1
/v1/auth/login
/v1/category
와 같은 꼴을 만들 수 있게 되었다.
* 이후 /api/v1, /api/v1/auth/login 등 으로 작동하도록 바꿨습니다. 버전명을 바꿀 때마다 설정을 계속 바꿀 순 없으니까요.
3. winston을 활용한 로깅
4. 에러처리기와 HttpException 에러
jeonghwan-kim.github.io/node/2017/08/17/express-error-handling.html
4개의 인자를 취하는 미들웨어를 "오류 처리 미들웨어"라고 부르는데 오류 발생시에만 호출됩니다.
다른 비즈니스 로직 중에 next(new Error()) 꼴을 반환하면, 오류 처리 미들웨어로 빠집니다.
app.use(function (error, req, res, next) {
// 여기에 도달할 것입니다
res.json({ message: error.message })
})
HttpException을 만든 후 미들웨어의 error 인자로 사용합니다.
class HttpException extends Error {
public status: number;
public message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
export default HttpException;
import { NextFunction, Request, Response } from 'express';
import { logger } from '../utils/logger';
import HttpException from '../exceptions/HttpException';
const errorMiddleware = (error: HttpException, req: Request, res: Response, next: NextFunction) => {
try {
const status: number = error.status || 500;
const message: string = error.message || 'Something went wrong';
logger.error(`StatusCode : ${status}, Message : ${message}`);
res.status(status).json({ message });
} catch (error) {
next(error);
}
};
export default errorMiddleware;
HttpException는 next(new HttpException) 꼴이나 throw new HttpException 꼴로 편하게 사용하실 수 있습니다.
const authMiddleware = async (req: RequestWithUser, res: Response, next: NextFunction) => {
try {
... 어떤 로직
if (findUser) {
req.user = findUser;
next(); // 일반 통과
} else {
next(new HttpException(401, 'Wrong authentication token')); // 에러 처리기 이동
}
} else {
next(new HttpException(404, 'Authentication token missing')); // 에러 처리기 이동
}
} catch (error) {
next(new HttpException(401, 'Wrong authentication token')); // 에러 처리기 이동
}
};
export default authMiddleware;
class AuthService {
public users = UserEntity;
public async signup(userData: CreateUserDto): Promise<User> {
if (isEmpty(userData)) throw new HttpException(400, "You're not userData");
... 로직
5. swagger 문서화
귀찮긴해도 api 명세서 작성은 프로젝트가 커지면 필수입니다!
세팅 자체는 어렵지 않으니 만들어줍시다.
그 외 자잘한 팁들
* eslint + prettier
.prettierrc를 eslintrc.js에 넣었습니다.
yarn add eslint prettier -D
yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
yarn add eslint-config-prettier eslint-plugin-prettier -D
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
plugins: ['prettier', '@typescript-eslint'],
rules: {
'prettier/prettier': [
'error',
{
singleQuote: true,
semi: true,
useTabs: false,
tabWidth: 2,
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid',
},
],
},
parserOptions: {
parser: '@typescript-eslint/parser',
},
};
제대로 동작하기 위해서 아래처럼 vscode setting.json을 바꿔주고, 저장 시 포매팅 기능은 꺼둡시다.
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.alwaysShowStatus": true,
"eslint.workingDirectories": [
{"mode": "auto"}
],
"eslint.validate": [
"javascript",
"typescript"
],
* .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
* .huskyrc를 이용해서 master push를 막는 훅을 달아놓거나 lint를 돌리는 등의 필요한 작업을 세팅해놓자.
changhoi.github.io/posts/etc/husky/
library.gabia.com/contents/8492/
{
"hooks": {
"pre-commit": "npm run lint"
}
}
* 미들웨어는 원하는 대로 사용하면 되는데 대체적으로 아래와 같이 구성해주면 큰 무리는 없었다.
물론 프로덕션 환경에서는 morgan 옵션을 바꾸고 cors의 오리진을 특정 도메인으로 한정하는 작업이 필요하긴 하다.
this.app.use(morgan('dev'));
this.app.use(cors({ origin: true, credentials: true }));
this.app.use(hpp());
this.app.use(helmet());
this.app.use(compression());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(cookieParser());
'Node, Nest, Deno > 🆕 Node.TS' 카테고리의 다른 글
type-node-graphql boilerplate 제작 (0) | 2020.10.20 |
---|---|
node(express) + Typescript 세팅 (outdated) (0) | 2020.06.24 |