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 코드로 트랜스파일한 다음에 실행하도록 하는 설정이다.
•
그 중에서 experimentalDecorators와 emitDecoratorMetadata 속성은 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 애플리케이션에서는 파일 하나에 클래스를 하나만 만든다.
•
그래서 AppController와 AppModule을 별도의 파일로 추출해야 한다.
•
파일명 규칙
◦
형식: 이름.타입.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!”라는 응답을 받게 된다.