Search

1. NestJS 기초

1. 왜 NestJS를 사용하는가?

명확한 디자인 패턴
훌륭한 문서화
유명한 패키지와 뛰어난 통합
의존성 주입을 통한 쉬운 테스트 및 코드 재사용

2. 목표

Nest 어플리케이션을 만들 때는 Nest CLI라는 툴을 사용
Nest CLI는 터미널에서 새 프로젝트를 생성하고 실행하는 툴
우리는 백지 상태부터 Nest 프로젝트를 만드는 것이 목표
그래야 NestJS의 내부가 어떻게 작동하는지 탐구할 수 있다!

3. 프로젝트 세팅

3.1. 의존성 설치

프로젝트 폴더 생성
mkdir scratch cd scratch
Shell
복사
의존성 설치
npm init -y npm install @nestjs/common@7.6.17 @nestjs/core@7.6.17 @nestjs/platform-express@7.6.17 reflect-metadata@0.1.13 typescript@4.3.2
Shell
복사
Nest는 아주 거대한 의존성 목록을 갖게 된다.
우리의 경우엔 백지 상태부터 모든 걸 연결할 것이기 때문에 아주 작은 의존성 몇 개만 있으면 된다.
1.
@nestjs/common
NestJS 애플리케이션을 만들기 위해 사용할 대부분의 함수, 클래스 등은 common 패키지에 담김
2.
@nestjs/platform-express
내부적으로 Nest 자체는 유입되는 요청을 처리하지 않는다.
Nest는 외부의 어떤 implementation에 의존해서, HTTP 요청을 대신 처리하도록 한다.
우리의 Nest 서버 안에서 우리는 코드 안에 어떤 장소를 마련해서 그곳에 어떤 HTTP implementation을 삽입할 것인지 정해야한다.
= 일종의 서버를 제공하는 것
그 서버는 여기서 유입되는 요청을 처리할 것이고, 외부로 나가는 응답을 제공할 것이다.
현재 Nest 세계에서는 HTTP 서버 implementation으로 Express 혹은 Fastify를 사용하고 있다. (기본값은 Express)
3.
reflect-metadata
데코레이터와 관련된 라이브러리
4.
TypeScript
JavaScript로도 Nest 애플리케이션을 작성할 수 있지만, 대부분은 TypeScript를 사용한다.

3.2. Typescript 컴파일 설정

프로젝트 루트 폴더에 tsconfig.json 파일을 생성한 뒤 아래 json을 작성한다.
{ "compilerOptions": { "module": "commonJS", "target": "es2017", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
JSON
복사
TypeScript 컴파일러의 역할은 우리의 프로젝트를 일반적인 JavaScript 코드로 트랜스파일한 다음에 실행하도록 하는 설정이다.
그 중에서 experimentalDecoratorsemitDecoratorMetadata 속성은 Nest가 실제로 작동하도록 해주는 핵심 중의 핵심이기 때문에 반드시 이해할 필요가 있다.

3.3. Nest 모듈과 컨트롤러 생성

모듈과 컨트롤러는 기본적인 앱을 만드는 데 필요한 요소들이다.
먼저 모듈과 컨트롤러가 무엇인지 간단히 알아보자.
우리가 서버를 만든적이 있다면 위 다이어그램이 상당히 친숙할 것이다.
어떤 HTTP 서버를 만들든지, 우리는 요청-응답 사이클이라고 하는 걸 만들게 된다.
1.
사용자가 서버에 요청을 보낸다.
2.
서버는 요청 안에 있는 약간의 데이터를 검증하고 요청된 경로에 따라 다르게 그 요청을 처리한다.
3.
응답을 만들어서 그 요청을 한 사용자에게 반환한다.
서버 애플리케이션이 사용자의 요청을 받아 처리하는 과정은 언어와 프레임워크마다 조금씩 차이가 있을 수 있지만, 기본적인 흐름은 대부분 유사하다.
서버는 요청을 검증하고, 인증 및 인가를 거친 후, 비즈니스 로직을 수행하며, 최종적으로 데이터베이스와 상호작용하여 필요한 데이터를 저장하거나 조회하는 방식으로 동작한다.
NestJS는 우리가 유입되는 요청을 처리하는 작업을 종합적으로 도와주는 도구들을 제공한다.
5가지 도구 말고도 Nest에는 더 많은 도구들이 존재한다.
실습을 진행보자.
src라는 이름의 새 디렉터리를 만든 다음 그 안에 main.ts라는 새 파일을 만든다.
main.ts는 모든 Nest 프로젝트에서 맨 처음 실행되는 파일이다.
보통 앱을 시작하고 특정한 포트에서 트래픽을 리스닝하기 시작하는 코드를 작성한다.
보통은 모듈과 컨트롤러를 별도의 파일에서 만들지만, 실습을 위해 main.ts 파일 안에서 곧바로 모듈과 컨트롤러를 작성한다.
AppController 생성
// src/main.ts import { Controller, Module } from "@nestjs/common"; @Controller() class AppController {}
TypeScript
복사
컨트롤러를 만들기 위해 AppController라는 이름의 클래스를 만든다.
그리고 그 바로 위에 @ 기호를 넣고 Controller() 데코레이터를 호출한다.
이는 애플리케이션 안에서 컨트롤러 역할을 할 클래스를 생성하려 한다고 Nest에게 알려주는 역할을 한다.
이 클래스는 유입되는 요청을 처리하고 라우팅할 클래스이다.
Nest는 데코레이터를 상당히 많이 사용하는데, 우선은 이 데코레이터를 넣기만 하고 실제로 하는 일에 대해서는 나중에 알아본다.
GET 라우터 생성
import { Controller, Module, Get } from "@nestjs/common"; @Controller() class AppController { @Get() getRootRoute() { return "hi there!"; } }
TypeScript
복사
이 클래스 안에서 우리는 다양한 메서드를 추가한다.
각각의 메서드는 유입되는 요청을 종류 별로 하나씩 처리하도록 되어 있다.
예를 들어 만일 우리가 애플리케이션 루트 경로에 대한 GET 요청을 처리하고 싶으면 getRootRoute() 같은 새로운 메서드를 추가하면 된다.
그렇게 하기 위해 우린 common 라이브러리로부터 또 다른 헬퍼인 @Get() 데코레이터를 호출해야 한다.
GET이라는 HTTP 메서드를 갖고 유입되는 요청에 대응하는 라우트 핸들러를 만들 수 있다.

3.4. Nest 앱 시작하기

모듈 설정하기
우리가 만드는 모든 애플리케이션에는 반드시 모듈이 하나 있어야 한다.
모듈을 만들기 위해 컨트롤러를 만들었던 것과 비슷하게 데코레이터를 적용한다.
@Module({ controllers: [AppController], }) class AppMoudle {}
Python
복사
@Module() 데코레이터를 사용할 때 우리는 설정 옵션(객체)를 넣어줘야 한다.
속성은 controllers가 될 것이고, 우리 애플리케이션에 있는 모든 컨트롤러를 여기 나열한다.
이제 애플리케이션이 시작될 때마다 Nest는 이 AppModule을 확인하고 여기 나열된 모든 컨트롤러를 검색할 것이다.
그리고 모든 컨트롤러 클래스의 인스턴스를 자동으로 생성한다.
그리고 나서 우리가 사용한 @Get()과 같은 데코레이터를 살펴보고, 라우트 핸들러를 설정한다.
애플리케이션이 시작될 때 실행될 함수 정의하기
import { NestFactory } from "@nestjs/core"; async function bootstrap() { const app = await NestFactory.create(AppMoudle); await app.listen(3000); } bootstrap();
Python
복사
마지막으로 애플리케이션이 실행될 떄마다 실행될 함수인 bootstrap()을 정의한다.
함수 안에 AppModule로부터 새로운 Nest 애플리케이션을 생성할 NestFactory.create(Appmodule)을 호출하여 app 인스턴스를 생성한다.
이후 await app.listen(3000);을 호출하여 애플리케이션이 3000번 포트에서 실행되도록 설정한다.
bootstrap()를 실행해서 애플리케이션의 인스턴스를 생성하고 유입되는 트래픽을 리스닝 하라고 할 것이다.
우리가 작성한 전체 코드는 다음과 같다.
import { Controller, Module, Get } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; @Controller() class AppController { @Get() getRootRoute() { return "hi there!"; } } @Module({ controllers: [AppController], }) class AppMoudle {} async function bootstrap() { const app = await NestFactory.create(AppMoudle); await app.listen(3000); } bootstrap();
TypeScript
복사
테스트
터미널에 다음과 같은 명령어를 입력하면 다음과 같은 로그를 확인할 수 있다.
npx ts-node-dev src/main.ts
TypeScript
복사
이제 웹 브라우저를 열어서 localhost:3000에 요청을 보내면 “hi there!” 이라는 문자열을 볼 수 있다.

4. 파일 명명 규칙

지금은 모든 코드가 main.ts 파일 안에 있다.
보통 Nest 애플리케이션에서는 파일 하나에 클래스를 하나만 만든다.
그래서 AppControllerAppModule을 별도의 파일로 추출해야 한다.
파일명 규칙
형식: 이름.타입.ts
파일명은 해당 클래스 또는 기능의 역할을 명확하게 설명해야 한다.
파일과 클래스 이름은 반드시 일치해야 한다.
클래스명 규칙
한 파일당 하나의 클래스만 포함 (일부 예외 가능).
클래스명은 해당 기능을 설명하는 단어와 역할을 포함해야 한다.
(예: AppController, UserService, AuthModule)
PascalCase(파스칼 케이스) 형식을 사용
리팩토링
src 폴더 안에 다음과 같이 파일을 나누고 코드를 리팩토링 해준다.
1.
app.controller.ts
import { Controller, Get } from "@nestjs/common"; @Controller() export class AppController { @Get() getRootRoute() { return "hi there!"; } }
TypeScript
복사
2.
app.module.ts
import { Module } from "@nestjs/common"; import { AppController } from "./app.controller"; @Module({ controllers: [AppController], }) export class AppMoudle {}
TypeScript
복사
3.
main.ts
import { NestFactory } from "@nestjs/core"; import { AppMoudle } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppMoudle); await app.listen(3000); } bootstrap();
TypeScript
복사

5. 라우팅 데코레이터

@Controller() 데코레이터와 @Get() 데코레이터에 대해 조금 더 자세히 알아보자.
기존의 코드에서는 두 데코레이터에 인자를 넣지 않았지만, 사실은 다음과 같이 인자를 추가할 수 있다.
import { Controller, Get } from "@nestjs/common"; @Controller("/app") export class AppController { @Get("/hello") getRootRoute() { return "hi there!"; } @Get("/bye") getByeThere() { return "bye there!"; } }
TypeScript
복사
@Controller 데코레이터로 컨트롤러의 기본 경로를 지정할 수 있으며, 클래스 내의 모든 핸들러 메서드는 이 기본 경로를 기반으로 동작한다.
예를 들어 위의 코드에서 @Controller('/app')을 사용하면, 해당 컨트롤러 내 모든 엔드포인트는 /app을 기반으로 동작하게 된다.
@Get() 데코레이터는 특정 경로를 인자로 전달하면 해당 경로에 대한 요청을 처리한다.
만약 인자를 전달하지 않으면 @Controller에서 설정했던 기본 경로(/app)로 매핑된다.
위 코드의 라우트 동작을 정리하면 다음과 같다.
HTTP 메서드
경로
실행 메서드
반환 값
GET
/app/hello
getRootRoute()
"hi there!"
GET
/app/bye
getByeThere()
"bye there!"
즉, /app/hello 경로로 GET 요청을 보내면 “hi there!” 라는 응답을 받게 되며, /app/bye로 GET 요청을 보내면 “bye there!”라는 응답을 받게 된다.