01. Nest.js 프로젝트 초기 세팅하기

SuJeong·2022년 8월 15일
9
post-thumbnail

엘리스 부트캠프가 끝나고 친해진 팀원들과 함께 궁금해약 프로젝트를 하기로 했다.

와! 새로운 프로젝트!

처음 이 알약 프로젝트는... 엘리스 마지막 프로젝트 때 현타가 심하게 와서 혼자 개인 프로젝트나 하려고 기획도 없이 대충 피그마로 디자인을 했었다(...) 이후 팀원들의 관심과 열정으로 점점 기능이 많은 추가되었고 엘리스가 끝나고 함께 프로젝트를 시작하여 지금의 궁금해약이 되었다!

공부도 할겸 개발 과정을 천천히 정리하려고 한다. 대부분 공식문서와 구글링을 참고하여 하나씩 공부하면서 작성할 예정이다.

기본적인 스택은 Nest.js이고, 엘리스 마지막 프로젝트때 공부했던 것을 바탕으로 기본 세팅을 했다.

사용한 IDE는 Visual Studio Code이다.

왜 Nest.js를?

Node.js의 Express는 Restful API를 만드는 강력한 프레임워크이지만 정해진 구조(아키텍처)가 딱히 없다. 3계층 구조(3 Layer Architecture)와 모듈화처럼 꾸준히 권고되는 구조는 있었지만, 프로젝트 설계가 제각각이고 명확하지 않는 것이 현실이었다.

Nest는 Node.js의 부족한 아키텍처를 위해 등장한 프레임워크이다(Express 대신 Fasify 위에 쓰기도 가능하다고 한다).

공식문서에 따르면, 테스트 가능성이 높고 확장 가능하며 느슨하게 결합되고 유지 관리가 쉬운 애플리케이션을 만들 수 있도록 하는 즉시 사용 가능한 애플리케이션 아키텍처를 제공한다고 한다.

쉽게 말해 개발을 더 용이하게 하는 아키텍처를 제공하는 셈이다.

또한 Typescript를 사용하려면 추가 설정을 해야하는 Express와는 달리, Nest에서는 Typescript가 기본 설정이다(바닐라JS로도 작성 가능하긴 하다).

타입이 없는 JS와는 다르게 타입 오류로 인한 런타임 오류를 잡아주는 TS는 여러모로 편리하다.

개인적으로 나는... Express의 아키텍처에 불만이 많았고 Swagger 같은 걸 추가할 때 설정해야하는 것이 너무 많아서 답답했다. 엘리스할때 기술스택으로 Nest를 추천해주신 코치님은 Swagger를 데코레이터를 활용해서 좀 더 간단히 사용할 수 있다고 설명해주셨다.그 말에 설득당했 어쨌든 이러한 장점을 가진 Nest를 사용하지 않을 이유가 없었다!

typescript는 사실 큰 프로젝트를 해보지 않아서 느낌이 크게 오진 않았다... 그냥 validation 할때 좋을 것 같았다. 조금 써보니까 이제 코드를 짜면서 미리 타입 에러를 예방할 수 있다는 점과 코드를 읽기도 점점 편해져서, type이 없으면 오히려 불편할 듯 하다.그전엔 어떻게 썼더라

Nest.js 프로젝트 생성하기

Nest를 시작하는 방법은 아주 간단하다. Nest CLI를 활용하면 Nest 아키텍처 패턴을 쉽게 만들 수 있다.

$ npm i -g @nestjs/cli

새로운 Nest project 폴더를 자동으로 생성해서 사용할 수도 있지만, 프로젝트 준비를 위해 github 등을 미리 연동해두었다면 이미 폴더가 있을 것이다.

그렇다면 './로 현재의 폴더에 프로젝트를 생성할 수도 있다.

$ nest new [my-nest-project]
$ nest new ./

이렇게 생성된 프로젝트는 표준 모드(Standard Repo)라고 하는데, 프로젝트 하나에서 하나의 서비스가 완성될 수 있는 구조이다. 다른 외부 프로젝트에는 의존하지 않는 다는 특징이 있다.

반대로 Mono Repo 모드가 있는데, MSA화를 하여 분리된 프로젝트가 공통된 코드를 필요로 할 때 그 코드를 일종의 라이브러리처럼 사용하는 프로젝트 구성 방식이다. github에서 Nest 코드 구경하다보면 가끔 보인다. 나도 모르고 싶었다

공식문서에서 가져온 Mono Repo 구조이다. apps에 여러 서비스가 각각 들어가고 공통으로 사용할 코드는 libs에 들어간다. 공통 코드는 db나 error 처리 등이 될 수 있다.

다음 프로젝트할 때 적용해보면 좋을 것 같다! 공식문서에서 더 자세히 볼 수 있다.

실행 방법

표준 모드 Nest.js를 실행하는 방법은 package.json에서 확인할 수 있다.

"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",

그 중에서도 --watch 옵션은 소스코드 변경을 감지하여 코드를 저장할 때 마다 서버를 다시 구동시켜 주기 때문에 개발 단계에서는start:dev를 주로 사용한다.

$ npm run start:dev

저장만 하면 서버 재시작이 되니 개발하면서 사용하기 아주 편하다!

Nest.js 기본 구조

표준 모드 Nest가 생성하는 기본 구조이다.

├── README.md
├── nest-cli.json
├── node_modules
├── package-lock.json
├── package.json
├── .gitignore
├── .eslintrc.js
├── .prettierrc
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

package.json는 Express와 같은 방식으로 패키지 의존성 관리를 한다.
node_modules는 패키지들이 실제로 설치하여 보관하기 때문에 용량 문제로 보통 .gitignore에 포함하여 깃 리포지토리에 공유하진 않지만, package-lock.json은 패키지를 설치할 때 정확한 버전과 서로간의 의존성을 표현하기 때문에 개발 환경을 공유를 위해 깃 리포지토리 공유하는 것이 좋다.

해두면 좋은 Nest.js 기본 세팅

미리 해두고 프로젝트에 들어가면 좋다. 대부분 초기 세팅으로 했던 것들이지만... 일부는 프로젝트를 진행하면서 필요성을 느꼈던 세팅을 함께 정리했다.

1. CORS 활성화

CORS 패턴은 한 도메인 또는 Origin의 웹 페이지가 다른 도메인을 가진 리소스에 접근할 수 있게 하는 보안 메커니즘이다. 클라이언트에서 도메인이 다른 서버에서 제공하는 API를 여럿 사용하는 경우 보안을 위해 사용하며 명령과 조회를 분리하여 성능과 확장, 보안성을 높인 아키텍쳐 패턴이라고 할 수 있다.

클라이언트에서 HTTP 요청의 헤더에 Origin을 담아 서버에 전달하고 서버는 Access-Control-Allow-Origin을 응답 헤더에 담아 클라이언트에게 전달한다.

이때 클라이언트에서 OriginAccess-Control-Allow-Origin를 비교하여 차단할지 말지를 결정한다.

CORS를 활성화하지 않으면 클라이언트는 서버의 요청을 차단해버려서 CORS 에러가 발생하기 때문에 서버에서 CORS 허용을 해주어야한다.

main.ts에 설정해주면 된다.

async function bootstrap() {
  	const app = await NestFactory.create(AppModule);
	app.enableCors();   // cors 활성화
  	await app.listen(3000);
}
bootstrap();

나중에 cookie 쓸 때 옵션을 더 줘야하긴 하지만 그 전까진 이거만 해도 CORS 에러를 충분히 잡아줄 수 있다.

2. ValidationPipe

파이프Pipe는 컨트롤러 라우터 핸들러가 처리하는 작업 도중 메서드가 호출 되기 직전에 메서드를 대상으로 하는 인수를 수신하고 작동하는 클래스로, 보통 변환Transformation이나 유효성 검사Validation을 위해 사용한다.

Transformation은 입력된 데이터를 원하는 형식(타입 등)으로 변환하는 것이고, Validation은 입력된 데이터의 타입이나 형태가 기준에 유효하지 않을 때 예외처리를 하는 것이다.

ValidationPipe를 사용하면 코드 중복 없이 모든 요청에 대해 validation이 가능해지기 때문에 적용했다.

먼저 종속성을 설치해야한다.

$ npm i --save class-validator class-transformer

옵션을 주어 원하는 Validation을 적용할 수 있다. 내가 적용한 옵션은 총 3개이다.

  • transform : 네트워크를 통해 받는 페이로드가 DTO 클래스에 따라 지정된 개체로 자동 변환되도록 하는 옵션

  • whitelist : 페이로드와 DTO 클래스를 비교해 수신해서는 안되는 속성을 자동으로 제거하는 옵션(유효성이 검사된 객체만 수신)

  • forbidNonWhitelisted : 허용하지 않은 속성을 제거하는 대신 예외를 throw하는 옵션

모든 endpoint에서 잘못된 데이터를 받지 않도록 보호하려면, main.ts에 global로 설정을 해주면 된다.

main.ts

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

선택한 옵션을 적용하였다.

모든 메서드가 호출되기 전에 validation을 자동으로 적용할 수 있게 된 것이다.

3. 환경 변수 설정

환경변수는 프로세스가 컴퓨터에서 동작하는 방식에 영향을 미치는 동적인 값들의 모임이다.

응용 프로그램은 종종 다른 환경에서 실행되고 환경에 따라 바뀌는 설정을 사용해야 할때가 있다. 개발 및 테스트 환경에서 빠르게 다루기 위해 여러 환경 별로 환경 변수를 각각 설정하여 이 문제를 해결할 수 있다.

보통 외부에서 전역으로 정의된 환경 변수는 process.env로 Node.js 내부에서 확인할 수 있다. .env에서 키와 값 쌍을 보유하여 각 환경을 나타내는 파일을 사용하고 환경 마다 올바른 .env 파일을 교체해주면 된다.

또한 개발과정에서 사용되는 고유한 key값 등 민감한 정보의 보안을 위해 .env를 깃 리포지토리에 공유해서는 안된다.

이러한 환경변수를 관리하는 방법은 dotenv를 쓰는 방법과 Nest에서 제공하는 @nestjs/config 라이브러리를 사용하는 것이 있다.

공식문서를 보면 Nest에서 환경변수를 설정하는 가장 좋은 방법이 @nestjs/config를 이용하는 것이라고 한다. 사실 처음 초기세팅을 할때 express에서 하던 것처럼 별 생각없이dotenv로 했었지만 나중에야 발견하고 고쳤다..

하는 김에 환경 변수의 유효성을 검사하는 Joi와 플랫폼 표준화를 위한 cross-env도 함께 사용한다. cross-env의 경우, 여러 env 파일을 사용해야할 때 유용하게 사용할 수 있다.

종속성을 설치해준다.

$ npm i --save @nestjs/config cross-env joi

먼저 환경 변수를 저장할 .env 파일을 만들어준다.

.development.env

NODE_ENV=development
SERVER_PORT=5000

현재 개발 환경이라는 것과 프론트에서 3000 포트를 사용해서 서버 포트를 5000으로 지정하는 환경 변수이다.

그 다음으로 환경 변수의 유효성을 검사하기 위한 파일을 만들어준다.

utils/validation.ts

import * as Joi from 'joi';

export const validation: Joi.Schema = Joi.object({
  NODE_ENV: Joi.string().valid('development', 'production').required(),
  SERVER_PORT: Joi.number().required(),
}).options({
  abortEarly: true,
});

NODE_ENV와 SERVER_PORT가 각각 string, number 타입이라는 것과 required 속성을 지정해주었다.

joi options으로 abortEarly를 주면 첫 번째 오류가 발생하면 즉시 유효성 검사를 중지하고 모든 오류를 반환한다.

여기서 주의해야할 점은 Joi를 import할 때 import Joi from 'joi';가 아닌 import * as Joi from 'joi';로 추가해야한다는 것이다.이것 때문에 계속 오류난다

이제 app.module.ts에서 해당 validation 파일을 불러 환경 변수 유효성 검사를 할 수 있다.

utils/index.ts

export * from './validation';

index.ts에 validation.ts를 추가해두면 validation을 불러올때 utils 만으로 불러올 수 있다. import를 할때 코드가 간소화되는 효과도 있어서 좋다.

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { validation } from './utils';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath:
        process.env.NODE_ENV === 'production'
          ? '.production.env'
          : process.env.NODE_ENV === 'development'
          ? '.development.env'
          : '.env',
      isGlobal: true,
      validationSchema: validation
      )}
   ]
})
export class AppModule {}

보통 루트 디렉터리에 있는 .env 파일을 찾는데, 다른 경로를 지정하려면 envFilePath에 option을 줘야한다.

나의 경우 개발 환경인 NODE_ENV가 development일때 .development.env를 사용하고, 배포 환경인 NODE_ENV가 production일때 .production.env를 사용하도록 설정했다.

다만 둘다 없을땐 .env를 사용하도록 하였다. 이 부분은 뒤에 prisma를 설정할때 DB url이 .env에 저장되기 때문에 설정해 두었다.

또한 다른 모듈에서 ConfigModule를 사용해야하기 때문에 전역으로 설정해야한다. isGlobal 옵션으로 설정해주었다.

앞서 만들어준 validation을 validationSchema에서 불러와 유효성 검사를 실시하도록 해준다.

나중에 yml 파일을 사용하게 된다면 yml 파일 유효성 검사도 추가할 수 있다. 나는 docker compose 파일을 validation하기 위해 적용했다. 공식문서에 나와 있다공식문서만세

환경 변수로 지정한 서버 포트로 어플리케이션을 실행할 수 있다.

main.ts

const configService = app.get(ConfigService);
const PORT = configService.get('SERVER_PORT');

await app.listen(PORT);

환경 변수를 설정하고 유효성을 검사하는 것은 초기에 꼼꼼하게 해둘 수록 편한 것 같다. 프로젝트 한창 진행하다가 환경 변수 설정 방식을 바꾸고 config 파일이나 만들고 있는 바보 같은 사람이 되지 말자.

자꾸 바꿔서 죄송해요....

4. Logger

Log는 프로그램이 실행되는 동안 일어나는 정보를 기록하는 것이다. Nest에는 내부 logger가 제공되어서 이 logger를 활용할 수 있다.

개발 환경에서 실행했을때 사용하는 url을 logger로 찍어보았다.

main.ts

import { Logger } from '@nestjs/common';

if (configService.get('NODE_ENV') === 'development') {
  Logger.log(`Application running on port ${PORT}, http://localhost:${PORT}`);
}

이렇게 별다른 설정 없이도 log를 편하게 찍을 수 있다.

log말고도 error, warn, debug, verbose 등 다양하게 찍을 수 있다. Nest 프로젝트를 할때 초반에 가져가면 좋은 기술인 것 같다.

5. 예외 필터

예외 처리는 프로그램 실행 흐름상 오류가 발생했을 때, 그 오류를 대처하는 방법이다. Nest에서는 제대로 처리되지 못한 예외를 처리하는 예외 레이어가 있다. HTTP 상태 코드인statusCode와 status에 기반한 HTTP 에러에 대한 설명인 message를 JSON 형태로 응답을 받는다.

처음엔 pipe와 filter가 비슷한 역할을 한다고 생각했는데 공식문서에도 그 점을 오해하지 말라고 설명해두었다.

메서드가 호출되기 전에 작동하는 pipe와는 다르게 filter는 client로 보내기 직전에 거친다. 그런 점에서 예외 처리를 하기에 좋다는 것이 이해가 간다.

예외 처리를 하는 필터를 예외 필터라고 한다. 여기서 예외가 발생했을때 모든 예외를 잡아 응답으로 요청했던 url과 시각을 콘솔에 출력하는 커스텀 예외필터를 소개하고 있다.

모든 모듈에서 사용할 것이니 common폴더에 만들어주었다.

common/filters/HttpExceptionFilter.filter.ts

import { ArgumentsHost, Catch, ExceptionFilter, HttpException, InternalServerErrorException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();

    if (!(exception instanceof HttpException)) {
      exception = new InternalServerErrorException();
    }

    const response:any = (exception as HttpException).getResponse();

    const log = {
      timestamp: new Date(),
      url: req.url,
      response,
    };

    Logger.log(log);

    res.status((exception as HttpException).getStatus()).json(response);
  }
}

@Catch는 처리되지 않은 모든 예외를 잡기 위해 사용한다.

예외는 Nest에서 HttpException을 상속받는 클래스 들로 제공해주기 때문에 알 수 없는 에러일 경우 서버 에러로 처리를 해준다.

응답으로 요청했던 url과 시각을 로그로 표시해주고 해당 에러를 response에도 보내준다.

이 예외 필터를 전역적으로 애플리케이션 전체에 적용할 수 있다.

main.ts

app.useGlobalFilters(new HttpExceptionFilter());

미처 예외 처리가 되지 않은 에러가 발생하면 서버 에러로 던지고 log를 찍어주는 예외 필터를 적용했다.

다 좋은데 프로젝트 하다보면 서버 에러가 정말 자주 나타난다! 정말 자주!! 그래서 이게 어디서 나는 에러인지 도통 알 수가 없다... 서비스 만들면서 에러 처리를 꼼꼼하게 해줘도 역부족이다.

난 최선을 다했어...

이 부분은 에러처리를 잘못하고 있어서 생긴 문제였다. 지금은 해결했다!

6. 코드 포맷터 적용

협업을 하면 코드 컨밴션을 정하게 된다. 코드 컨밴션은 일종의 코드 작성 스타일 규칙인데 서로의 코드를 읽고 편하게 관리하기 위해 협업을 할때는 반드시 정하는게 좋다. 우리 프로젝트에서는 프론트와 백이 각각 컨밴션을 정했다.

코드 컨밴션을 개발자가 작성한 코드를 정해진 코딩 스타일로 자동으로 변환해주는 것이 코드 포매터이다.없이는 많이 귀찮다

prettier

대표적인 코드 스타일을 깔끔하게 만들어주는 코드 포맷터인 prettier이다. 줄바꿈이나 공백, 들여쓰기 등등 코드를 깔끔하고 일관되게 작성할 수 있게 도와준다.

.prettierrc

{
  "singleQuote": true,
  "tabWidth": 2, 
  "trailingComma": "all",
  "printWidth": 80, 
  "quoteProps": "as-needed",
  "arrowParens": "always",
  "endOfLine": "auto"
}

singleQuote를 적용하면 "이 아닌 '로 통일한다는 것이고, tab 너비, 후행 콤마 사용, 줄바꿈할 폭 길이, 객체 속성에 quote 사용, 화살표 함수 괄호 사용 방식, EoF(end of file) 방식을 설정해주었다.

eslint

prettier가 깔끔한 코드를 만들어준다면, eslint는 코드 구현 방식을 일관되게 만들어준다.

Nest에서는 eslint를 기본으로 제공한다. 딱히 바꾸는 것 없이 사용해도 좋다.

자동으로 prettier와 eslint 적용하기

저장을 할때마다 코드 포맷터가 적용되기를 바란다면 이 설정을 반드시 해줘야한다.

vscode의 extensions에서 prettier를 검색해서 설치해준다.

vscode의 extensions에서 eslint를 검색해서 설치해준다.

extension이 vscode에서 작동하도록 설정이 필요하다.

.vscode/settings.json

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}

저장할 때마다 prettier가 적용되고 eslint 규칙에 맞게 세팅해준다.

사용하지 않는 import 자동 삭제

vscode에서는 사용하지 않는 import를 자동으로 삭제하는 세팅도 가능하다.

단축키 ctrl+shift+p를 입력하고 Preferences:Configure Language Specific Settings을 선택한다.

사용 언어는 TypeScript으로 선택한다.

settings.json 파일이 열리면 아래 두가지 속성을 추가한다.

  "[typescript]": {
    "editor.codeActionsOnSave": {
      "source.organizeImports": true,
      "source.fixAll": true
    }
  }

저장을 할때마다 사용하지 않는 import는 자동으로 삭제된다.

초기 세팅 끝~

회고

공식문서와 참고서를 읽어보고 애매한 부분을 정리하여 도움이 많이 되었다.

다음에 Nest 초기 세팅 할때에도 참고할 수 있도록 꼼꼼하게 적었고, 만약 같은 스택으로 프로젝트를 시작하는 개발자분이 계시다면 도움이 되었으면 좋겠다.

reference

https://docs.nestjs.com/
https://wikidocs.net/book/7059

profile
backend developer / 꾸준히 배우고 적용하는걸 좋아합니다!

0개의 댓글