NestJS 서버 개발의 서막: 한 달의 여정
회사에서 내가 NestJS 서버 개발을 맡기 시작한 지 약 한 달이 흘렀다.
물론 NestJS 서버 개발 업무만 진행한 것은 아니지만, 이번 달에는 기본적인 서버 구조를 잡고 빠른 배포까지 진행하게 되었다.
개발 문화도 나름 잘 구축해가고 있다. (커밋 컨벤션, 이슈, 프로젝트 이용)
- 시작은 아래는 어렴풋이 잡혀가는 나의 백엔드 서버 구조 이미지와

- 같이 익어가는 내 Node.js 옵시디언 볼트 (사실 Node.js라고 정리하지만 자바스크립트(DOM), 타입스크립트, 모듈, sql, html, css 등등 다 짬뽕되어 있다…)

여튼 이번 달에 내 기억에 남는 기술들은 아래와 같았고, 제외하고도 추가적으로 몇 가지를 더 사용했다. 몇 가지 자잘한 것들이 더 있지만 일단은 백로그처럼 적어두고 하나씩 더 구체화하면서 하나씩 개모임 때 이야기를 해보겠다.
지금까지 크게 생각나는 작업들
NestJS(서버)
- NestJS 라이프 사이클 이용 및 프로젝트 구축(폴더 정리 포함)
- class-validator를 사용한 데이터 유효성 검증
- 프로젝트의 여러가지 플러그인을 적용
- Firebase api 사용해서 인증 및 푸시 알림 기능 자동화
배포
- EC2: 인스턴스에 고정 ip 부여해서 배포했음
- Caddy: SSL 자동 인증해줌 → 도메인(nip.io 붙여줌) 무료 → nginx보다 빠르다는 이야기도…?(Route53 안썼음)
- PM2 + Monitor: 프로세스 관리 및 모니터링 → 컨테이너 2개로 reload를 사용한 low한 수준의 무중단 배포 구현, cors 프론트 로컬 + 프론트 배포 환경 뚫어놓기
- YML: env와 yml 파일로 development 환경과 production 환경 관리



NestJS의 생명 주기(Lifecycle)
NestJS는 애플리케이션이 부트스트랩되어 종료될 때까지 다양한 수명 주기(Lifecycle) 이벤트를 관리한다고 한다.
이를 통해 초기화, 실행, 종료 과정에서 적절한 로직 수행이 가능하다.
- 웬만한 프레임워크에 라이프사이클이 있지만 JSX가 친숙한 나로는 그냥
useEffect가 떠오른다.
라이프사이클 다이어그램

NestJS의 수명 주기는 다음과 같이 3가지 단계로 나눌 수 있다
- 초기화
- 실행
- 종료
| 수명 주기 후크 방법 | 설명 |
| --- | --- |
|
onModuleInit()| 모듈 종속성이 해결된 후 호출 | |onApplicationBootstrap()| 모든 모듈 초기화 후 연결 수신 대기 전에 호출 | |onModuleDestroy()| 종료 신호 수신 후 모듈 종료 시 호출 | |beforeApplicationShutdown()| 모든onModuleDestroy()가 완료된 후 호출 (Promise 처리) | |onApplicationShutdown()| 애플리케이션 종료 후 호출 |
enableShutdownHooks() 사용
종료 훅은 enableShutdownHooks()를 호출해야만 활성화된다. 이 메서드는 애플리케이션이 종료될 때, 시스템 신호를 수신하고 종료 프로세스를 진행할 수 있게 도와준다.
TypeScript
종료 후크 예시
종료 신호가 수신되면 아래와 같이 onApplicationShutdown() 메서드를 통해 시스템 신호를 받아 애플리케이션이 적절히 종료된다.
TypeScript
그렇다고 한다…
NestJS의 로직 수행 흐름
- 사실 위의 라이프사이클 메서드를 사용해서 서버를 직접 건드릴 일은 아직은 딱히 없었고, 여러 블로그의 우리들의 세분화된 NestJS 미들웨어 친구들의 동작방식을 설명하기 전에 기술되어 있어 때문에 잠시 공부해봤다.
아래 이미지는 NestJS 공식 사이트 이미지

NestJS는 미들웨어, 가드, 파이프, 인터셉터 등 다양한 단계로 요청을 처리한다.
- 각 단계에서 다양한 작업을 수행할 수 있으며, 요청이 처리되는 흐름은 다음과 같다.
- Request: 클라이언트의 요청이 들어오면, 요청이 아래의 단계들을 거친다.
- Middleware: 요청 전에 수행되는 로직. 주로 로그, 인증 등 선처리 작업을 수행.
- Guard: 인증이나 권한을 처리하는 단계로, 요청이 서비스에 도달하기 전에 검증한다.
- Pre-Interceptor: 함수 실행 전 데이터 가공이나 로깅 등의 작업을 수행.
- Pipe: 요청 데이터를 변환하고 유효성 검사를 수행한다.
- Controller: 요청을 처리하고 적절한 서비스 로직을 호출한다.
- Service: 핵심 비즈니스 로직이 처리된다.
- Post-Interceptor: 처리 후 응답 데이터를 가공하거나 성능 측정을 수행한다.
- Exception Filter: 예외 상황을 처리하며, 에러 메시지를 적절한 형태로 응답한다.
- Response: 최종적으로 클라이언트에게 응답을 보낸다.
친절한 NestJS: 미들웨어 같은 기능들
NestJS는 각 단계에서 인터셉터(Interceptor), 가드(Guard), 필터(Filter), 파이프(Pipe) 등의 역할을 지정해둔다.
인터셉터
인터셉터는 함수 실행 전후에 로직을 추가하거나 응답 데이터를 가공하는 데 쓰인다.
이것도 약간 JSX에 비유하자면, useEffect 처럼 응답 보내기 전이나 후에 "잠깐만, 나 할 거 있어" 하면서 로직을 끼워넣는 것과 같았다.
TypeScript
가드
가드는 요청이 실제로 처리되기 전에 인증, 권한 검사를 수행하는 데 사용된다.
TypeScript
사용은 아래와 같은 컨트롤러에서 @UseGuards 데코레이터를 사용해서 쉽게 사용한다.
TypeScript
필터
필터는 예외 상황에서 사용자에게 적절한 메시지를 반환할 수 있다.
TypeScript
위의 코드는 실제로 내가 사용할 때 class-validator에서 뱉는 오류 대신 특정 문자열을 반환시킬려고 적용한 커스텀 필터이다.
아래처럼 main.ts에 적용시켜서 사용가능하다.
TypeScript
파이프
파이프는 요청된 데이터를 변환하고 유효성을 검증하는 데 사용된다. 주로 데이터 변환(Data Transformation)이나 유효성 검사(Data Validation)를 처리한다.
파이프는 요청이 컨트롤러에 도달하기 전에 데이터를 변환하고 검증하는 역할을 하며, Handler-level, Parameter-level, Global-level로 나누어 사용할 수 있다
파이프와 class-validator의 차이
- 파이프와 class-validator는 비슷해 보일 수 있지만, 그 목적과 사용 방식이 조금 다르다.
- 사실상 거의 둘이 세트로 쓰긴한다.(공식 문서에도 같이 사용을 권장하고 있다는)
파이프
- 파이프는 주로 데이터 변환과 검증을 담당하며, 요청이 컨트롤러로 넘어가기 전에 데이터를 가공할 수 있다.
- 파이프는 전역(Global)으로 적용할 수 있으며, 핸들러나 특정 파라미터에서만 동작하도록 설정할 수 있다.
TypeScript
- 위 예시에서
ValidationPipe는 컨트롤러 메서드가 호출되기 전에 요청 데이터를 검증한다.
TypeScript
class-validator
- class-validator는 엔티티나 DTO에서 주로 사용되며, 데이터 유효성 검증을 처리하는 데 더 적합하다.
- class-validator는 주로 데이터 모델에 대해 구조적 검증을 수행하는 역할을 하며, 파이프가 데이터를 전처리하는 데 반해, class-validator는 엔티티나 DTO에 적용하여 유효성을 검사한다.
약간 스프링의 롬복을 떠올리게 만들었다.
TypeScript
- 위 예시에서는
CreateBoardDto에서title과description필드가 비어 있지 않은지 검증하며, class-validator는 이런 식으로 모델의 유효성을 보장한다.
파이프와 class-validator의 역할 분담
- 파이프는 요청이 컨트롤러에 도달하기 전에 데이터를 변환하거나 처리하는 데 주로 사용된다.
- class-validator는 주로 엔티티나 DTO에서 데이터 구조의 유효성을 검증하는 데 사용된다. 이러한 차이점을 명확하게 인식하고, 목적에 맞게 두 가지 도구를 조합하여 사용하면 훨씬 더 효율적으로 요청 데이터를 처리할 수 있다.
NestJS를 서버 개발하면서 설정한 프로젝트 플러그인들
1. 핫 리로딩 (Hot Reloading)
공부를 하다가 핫 리로딩을 발견하였고 이를 적용시키면서 DX를 크게 향상시켰다.
- 기본적으로 NestJS는 Express에서 사용하는 nodemon처럼 자동으로 서버를 재시작하지만, 핫 리로딩은 코드가 변경될 때마다 서버를 자동으로 재시작하는 것과 달리, 서버를 끄고 다시 켜는 과정을 거치지 않고 변경된 내용을 즉각적으로 반영할 수 있도록 해준다.
즉, 상태 유지가 되기때문에 서버 재시작 없이 코드 변경이 적용되므로, 세션이나 데이터를 다시 불러오는 등의 번거로운 작업을 줄일 수 있었다.
webpack-hmr.config.js 설정
핫 리로딩을 설정하기 위해 webpack-hmr.config.js 파일을 만들었다. 이 파일은 Webpack을 사용하여 모듈이 변경될 때마다 즉시 반영되도록 하는 설정을 포함한다.
JavaScript
- 핫 리로딩 명령어 변경 package.json에 아래 명령어로 핫 리로딩을 실행할 수 있다.
JSON
2. typeorm-model-generator
Strapi로 설계된 기존 데이터베이스에서 ORM을 위한 엔티티(Entity)를 쉽게 생성하기 위해 사용한 툴이다.
Strapi가 설계한 복잡하고 정리되지 않은 수많은 필드들을 직접 관리하지 않고도, 자동으로 엔티티를 생성할 수 있어 매우 유용했다.
Bash
- 이 명령을 사용하면 데이터베이스에서 필요한 엔티티를 자동으로 생성할 수 있다.
- 자동으로 생성된 엔티티는
output/entities폴더에 저장되었으며, 이를 통해 데이터베이스를 직접 확인하지 않아도 모델을 쉽게 관리할 수 있게 되었다.
나의 경우는 output/entities에서 복사하여 필요없는 필드는 걷어내고 src/entity로 옮겨 사용했다.
3. Swagger 적용
main.ts에서 Swagger 설정을 아래와 같이 추가할 수 있다. (데코레이터 붙이면 잘 만들어 준다.)
TypeScript
위 설정을 통해 Swagger 문서를 http://대충 배포 url/api-docs에서 확인할 수 있다.
TypeScript
만들면서 재밌었던 api: fcm 앱 푸시
-
어드민 인증, 유저 인증, 상품 api 개발, gnb api 개발 등을 만들었고 유지보수하지만 기억에 남는 작업은 fcm 자동화가 아닐까 싶다.
-
Firebase Cloud Messaging을 사용해 푸시 알림을 자동화한 것도 기억에 남는다. 푸시 알림을 보내는 백엔드를 처음으로 만들어보기도 했고, 내용 입력하고 버튼 딸깍으로 자동화해서 유저들에게 푸쉬알림을 보낼 수 있게 만들었다는 것이 재밌었다.
-
아래와 같이, db에서 유저 필터링해서
TypeScript
- 아래처럼 모듈화를 커쳐서 내용 입력하고 버튼 딸깍하면 파이어베이스 이용하여 푸시 알림을 전송하게 만들었다.
TypeScript
푸시알림 로그는 strapi 테이블에 연동해놔서 strapi에서 확인이 가능하다.
만료된 토큰은 삭제하는 건 혹시 몰라 아직 검토중이다.