[NestJS] NestJS 시작 (설치 & 구성요소 맛보기)
2024.01.22- -
이제 드디어 NestJS로 어떻게 백엔드 서버를 만들 수 있는지에 대해 알아보도록 하겠습니다. NestJS는 강력한 typing, interfaces, decorator와 같은 기능을 추가하는 TypeScript를 기반으로 구축됩니다.
NestJS는 Angular로부터 강한 영감을 받았고 실제로 Dependency Injection, modules, controller와 같은 대부분의 동일한 기능들을 제공합니다. Angular의 이러한 요소는 확장가능하도록 디자인 되어 개발자들이 NestJS의 기능을 확장하기 위해 자기들의 모듈과 플러그인을 만들 수 있다는 것을 의미합니다.
이번 포스팅에서는 NestJS 공식문서를 참고하여 우리가 앞서 배워왔던 개념들을 실제로 어떻게 적용할 수 있는지, 코드 위주로 살펴보도록 하겠습니다.
1. NestJS 설치
NestJS 애플리케이션 구축을 하려면 설치를 해주어야 합니다. NestJS는 Node.js 패키지 매니저인 npm을 사용하여 설치될 수 있습니다.
NestJS를 설치하기 위해 vscode의 터미널 창이나 윈도우 혹은 맥 자체 터미널 창을 열어 다음 명령어를 입력합니다.
npm install -g @nestjs/cli
이 명령어는 NestJS CLI를 컴퓨터 시스템에 전역적으로 설치됩니다. NestJS CLI는 우리의 NestJS 애플리케이션을 생성하고 관리하는데 사용할 수 있는 명령어 세트를 제공합니다.
설치가 완료되고 나면, 다음 명령어를 통해 NestJS가 잘 설치되었는지 확인해볼 수 있습니다.
nest --version
이 명령어는 시스템에 최근에 설치된 NestJS의 버전을 보여줍니다.
2. 가장 기본 NestJS 프로젝트 생성하기
이제 NestJS를 설치했으니 가장 기본이 되는 프로젝트를 만들어봅시다. 우리는 NestJS CLI를 사용해 만들어볼 것입니다.
NestJS CLI는 프로젝트를 생성하고 또 프로젝트에 새로운 기능들을 추가할 경우, 굉장히 다양한 옵션을 제공하여 관련 파일들을 직접 만들어줍니다. 아래 명령어를 통해 어떠한 CLI 명령어들이 있는지 볼 수 있습니다.
nest
nest
Usage: nest <command> [options]
Options:
-v, --version Output the current version.
-h, --help Output usage information.
Commands:
new|n [options] [name] Generate Nest application.
build [options] [app] Build Nest application.
start [options] [app] Run Nest application.
info|i Display Nest project details.
add [options] <library> Adds support for an external library to your project.
generate|g [options] <schematic> [name] [path] Generate a Nest element.
Schematics available on @nestjs/schematics collection:
┌───────────────┬─────────────┬──────────────────────────────────────────────┐
│ name │ alias │ description │
│ application │ application │ Generate a new application workspace │
│ class │ cl │ Generate a new class │
│ configuration │ config │ Generate a CLI configuration file │
│ controller │ co │ Generate a controller declaration │
│ decorator │ d │ Generate a custom decorator │
│ filter │ f │ Generate a filter declaration │
│ gateway │ ga │ Generate a gateway declaration │
│ guard │ gu │ Generate a guard declaration │
│ interceptor │ itc │ Generate an interceptor declaration │
│ interface │ itf │ Generate an interface │
│ middleware │ mi │ Generate a middleware declaration │
│ module │ mo │ Generate a module declaration │
│ pipe │ pi │ Generate a pipe declaration │
│ provider │ pr │ Generate a provider declaration │
│ resolver │ r │ Generate a GraphQL resolver declaration │
│ service │ s │ Generate a service declaration │
│ library │ lib │ Generate a new library within a monorepo │
│ sub-app │ app │ Generate a new application within a monorepo │
│ resource │ res │ Generate a new CRUD resource │
└───────────────┴─────────────┴──────────────────────────────────────────────┘
실제 개발을 진행할 때도 추가해야 하는 기능이 있다면 직접 폴더와 파일을 만드는 것이 아니라 CLI 를 사용하면 자동으로 구조를 갖춘 파일들을 생성해주기 때문에 굉장히 간편한 기능이라고 할 수 있습니다.
새로운 NestJS 프로젝트를 생성하려면, 다시 터미널을 열어서 우선 프로젝트를 만들고 싶은 폴더로 이동합니다. (cd 명령어 활용) 그 후, 다음 명령어를 입력합니다.
nest new new-project
이 명령어는 새로운 NestJS 프로젝트를 현재 위치의 new-project라는 이름의 폴더 안에 생성할 것입니다. new-project는 프로젝트 이름으로, 프로젝트 주제에 맞게 원하시는 이름으로 바꾸실 수 있습니다.
명령어 입력이 완료되면, 다음 명령어를 통해 new-project 폴더로 이동합니다.
cd new-project
이제 우리는 프로젝트 폴더 안에 들어왔으니, 다음 명령어를 통해서 애플리케이션을 시작할 수 있습니다. 프로젝트 생성 시 가장 기본적인 모듈이 제공되기 때문에 서버를 곧바로 시작할 수 있습니다.
npm run start:dev
이 명령어는 개발(development) 모드로 애플리케이션을 시작합니다. 터미널 창을 보면 애플리케이션이 시작했다는 표시가 나오게 됩니다.
- 뒤에 붙은 접미사 ':dev'는 우리가 서버를 시작할 때 nodemon 스크립트가 사용된다는 것을 의미하는데, nodemon은 코드의 변화를 감지하고 있다가 우리가 코드가 수정되었을 때, 이를 감지하여 자동으로 서버를 재시작하여 수정사항이 서버에 반영되도록 하는 기능입니다.
기본적으로 지금 내 컴퓨터에서 3000번 포트를 사용하고 있지 않다면 우리의 애플리케이션은 http://localhost:3000에서 실행중일 것입니다. 웹 브라우저를 열어 루트 경로('/') URL로 접속하면 "Hello, World!"를 확인할 수 있습니다.
3. 프로젝트 구조
새로운 프로젝트를 생성하면 초기에는 아래와 같은 기본 프로젝트 구조를 갖게됩니다.
프로젝트에서 제일 중요한 폴더는 아무래도 src 폴더입니다. 이곳에 우리가 찾고 싶은 모든 애플리케이션 코드가 존재하는 TypeScript 파일들이 존재하기 때문인데요. src 폴더로부터 우리는 다음 5가지 프로젝트 초기 셋업 파일을 확인할 수 있습니다:
- main.ts: 애플리케이션의 진입점(entry point). NestFactory.create()을 사용하여 새로운 Nest Application 인스턴스가 생성됩니다.
- app.module.ts: 애플리케이션의 root module에 대한 구현을 포함합니다.
- app.controller.ts: 하나의 라우트만을 갖고 있는 기본 NestJS 컨트롤러의 구현을 포함합니다.
- app.service.ts: 기본 서비스 구현을 포함합니다.
- app.controller.spec.ts: 컨트롤러에 대한 테스팅 파일입니다.
보시면 아시겠지만 기본적으로 controller와 service을 분리시켜 라우트와 비지니스 로직을 구분하는 계층 아키텍처를 따르고 있습니다. 다만 가장 기본 세팅에서는 도메인(model)과 관련된 부분은 보이지 않고, 또 데이터베이스를 사용할지 안할지 모르니 Persistence Layer가 맡고 있는 데이터베이스 영속성(persistence)과 관련된 파일들은 만들어지지 않은 것으로 보입니다. 해당 부분을 추가하는 방법은 뒤에서 다시 설명드리도록 하겠습니다.
main.ts 파일
코드를 살펴보면서 어떤 식으로 NestJS 애플리케이션이 구성되었는지 알아보도록 하겠습니다. 애플리케이션 실행과 관련해서는 main.ts 파일을 보면 됩니다.
이 파일은 NestJS 애플리케이션이 시작되는 진입 지점(entry point)가 됩니다.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
첫번째, NestFactory가 @nestjs/core 라이브러리로부터 import됩니다. 그에 더해 AppModule이 우리 프로젝트의 app.module.ts 파일로부터 import 됩니다.
두번째, bootstrap 함수가 구현되었으며, async로 지정되었습니다. 이 함수 내부에서 NestFactory.create() 메소드가 호출되고 root application module(기본적으로 AppModule)이 create() 함수의 인자로 넘겨집니다. 이를 통해 AppModule이 탑재된 새로운 NestJS 애플리케이션 인스턴스가 만들어지게 됩니다.
- 여기서 create() 메소드에 다른 모듈, 예를 들어, CatModule을 넣어주게 되면 CatModule이 root application module이 됩니다.
서버를 실행시키기 위한 다음 단계로, listen() 메소드를 호출하고 그 메소드의 인자로 웹 서버가 실행되어야 하는 포트를 넘겨줍니다(일반적으로 3000 포트 사용). llisten() 메소드는 서버가 성공적으로 시작했을 때 promise를 반환하기 때문에 해당 메소드를 호출할 때는 await 키워드를 붙여주어야 합니다.
마지막으로, 앞서 말했듯 이 파일은 진입점이기에 nest 서버를 실행시키는 명령어를 입력하면 이 파일이 실행됩니다. 그래서 반드시 이 파일에는 실행되어야 하는 코드가 필요한데 그 부분이 바로 마지막 줄의 bootstrap() 함수 호출인 것입니다.
- 참고로 bootstrap이란 단어는 우리가 흔히 '컴퓨터 부팅한다'할 때 그 부팅을 의미합니다. 따라서 '우리의 서버를 부팅한다', '우리의 서버를 켠다'라는 의미가 되겠네요.
app.module.ts
이제 다음으로 root application module의 구현을 살펴봅시다. root application module은 보통 AppModule의 이름으로 생성되는데요. 우리는 이것을 프로젝트 폴더에서 app.module.ts 파일을 통해 확인할 수 있습니다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
AppModule 클래스를 module로서 선언하기 위해서는 @nestjs/common 라이브러리로부터 import되는 @Module() 데코레이터를 해당 클래스에 붙여주면 됩니다. 세 가지 속성을 가진 객체가 이 @Module() 데코레이터로 넘어가야 합니다. 이 세 가지 속성은 다음과 같은 것들입니다:
- imports: 이 모듈에서 필요로 하는 providers를 export(내보내는)하는 import 모듈의 목록
- controllers: 인스턴스화 되어야 하는 이 모듈에서 정의된 컨트롤러 집합
- providers: Nest 주입자에 의해 인스턴스화 될, 그리고 최소 이 모듈 전체에서 공유될 수 있는 providers 모음
- exports: 이 모듈에서 제공되며 이 모듈을 import 하는 다른 모듈에서 사용가능하게 하는 providers의 부분집합으로, provider 자체나 provider의 token(provider value)을 사용할 수 있다.
애플리케이션에서 사용되는 모든 컨트롤러들은 AppModule의 속해야 합니다. 즉 그 컨트롤러들은 controllers 속성에 할당되는 배열의 요소여야만 한다는 의미입니다.
- 애플리케이션 초기 상태에서는, root module(즉, AppModule)에 단 하나의 컨트롤러만이 존재할 것입니다.
뒤에서 배우게 될 services 역시 AppModule에서 사용가능해야 하며, service의 경우에는 providers 속성에 할당되는 배열에 속해야 합니다.
4. NestJS에서 Controllers와 Routes를 정의하는 법
지난 포스팅에서 배웠듯 NestJS에서는 controller가 들어오는 HTTP 요청을 처리하고 응답을 반환하는 역할을 한다고 했습니다. Controller는 특정 URL 경로와 HTTP 메소드에 매핑되는 route를 정의할 수 있습니다.
새로운 controller를 만들기 위해서, 프로젝트 폴더 내에서 NestJS CLI를 사용하여 다음 명령어를 입력합니다.
nest generate controller cats
이 명령어는 cats라는 이름의 새로운 controller를 cats/ 폴더 밑에 cat.controller.ts라는 파일을 생성할 것입니다. 이 파일을 열면 아래와 같이 가장 기본 컨트롤러 클래스의 모습을 확인할 수 있을 것입니다:
// cats.controller.ts
import { Controller } from '@nestjs/common';
@Controller('cats')
export class CatsController {}
이 컨트롤러의 route에 대한 URL 경로를 구분짓는 route를 위해서 데코레이터가 붙여집니다(@Controller). 이 경우에 컨트롤러는 주소 뒤에 /cats로 시작하는 요청을 처리할 수 있게됩니다. @Controller 데코레이터는 @nestjs/common 라이브러리로부터 import 됩니다.
이 컨트롤러에 대한 라우트를 정의하기 위해서는, 클래스 내부에 아무 메소드를 추가하고 해당 메서드가 동작하기 원하는 HTTP 응답 메소드에 맞게끔 데코레이터를 붙여줍니다. 예를 들어, /cat으로 들어오는 GET 요청에 대한 라우트를 정의하려면, CatsController 클래스에 다음과 같은 메소드를 추가합니다.
// cats.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
이 예제에서 @Get() 데코레이터가 이 메소드는 GET 요청을 처리해야 함을 지정하고 메소드는 응답으로 보내질 문자열을 반환합니다.
다른 HTTP 메소드 데코레이터가 붙은 메소드를 추가하여 이 컨트롤러에 대한 추가적인 라우트를 계속해서 정의할 수 있습니다. 예를 들어, /cast로 들어오는 POST 요청에 대한 라우트를 정의하기 위해, CatsController 클래스에 다음 메소드를 추가합니다.
// cats.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
@Post() // POST 메서드를 추가
create(): string {
return 'This action adds a new cat';
}
}
이 예제에서 @Post 데코레이터가 이 메소드는 POST 요청을 다뤄야 한다고 지정해주고 응답으로 보내질 문자열을 반환합니다.
5. NestJS에서 Service와 Provider를 사용하는 방식
NestJS에서 services는 비지니스 로직을 다루는데 사용됩니다. 그리고 services는 controllers나 다른 services에 주입(import)될 수 있습니다.
Providers는 services를 위한 건물 벽돌과 같은 것이고 다른 컴포넌트로 주입(import)될 수 있는 클래스의 인스턴스나, 함수, 변수 등을 생성하는데 사용될 수 있습니다.
새 service를 만들기 위해 프로젝트 폴더에서 NestJS CLI 제공하는 다음 명령어를 입력합니다.
nest generate service cats
이 명령어는 cats라는 이름의 새로운 service를 cats.service.ts 파일로 생성할 것입니다. 이 파일을 열면 아래와 같이 가장 기본적인 service 클래스가 구현된 것을 확인할 수 있을 것입니다.
// cats.controller.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {}
이 서비스는 @Injectable() 데코레이터가 붙여졌는데, 이는 NestJS에게 "이 클래스는 다른 컴포넌트로 주입될 수 있게 해줘"라고 말하는 것과 같습니다.
이 서비스에 기능을 추가하려면, 역시 클래스 안에 메소드를 추가하면 됩니다. 예를 들어, 고양이 리스트를 반환하는 함수를 추가하고 싶다면, CatsService 클래스에서 다음 메소드를 추가합니다.
// cats.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
private cats = ['샴', '렉돌', '노르웨이 숲'];
findAll(): string[] {
return this.cats;
}
}
이 예제에서 cats 배열은 서비스의 private 속성이고 findAll() 메소드는 이 배열을 반환합니다.
- private은 '접근지정자'로 접근 범위를 지정하는 키워드인데, 이 클래스의 외부에서 이 서비스를 주입할 때 cats 변수에는 접근하지 못하고, 대신 그 변수를 반환하는 메소드를 대신 호출함으로써 cats를 가져오도록 하는 구조입니다. 이는 캡슐화의 기본 원칙입니다.
- 접근지정자를 따로 명시하지 않으면 기본적으로 public으로 설정되어 어디서든 접근가능한 상태가 됩니다.
이 서비스를 특정 컨트롤러에서 사용하기 위해서는, @Inject 데코레이터를 사용하여 사용하고 싶은 컨트롤러의 constructor(생성자)로 이 서비스를 주입하면 됩니다.
- 이는 이전에 배운 의존성 주입 개념으로 컨트롤러 내부에서 CatsService 클래스의 인스턴스를 생성하는 것이 아닌 생성자의 인자에 넣음으로써 NestJS가 자동으로 CatsService를 해당 클래스에 주입시켜주어 둘 간의 의존 관계를 느슨하게 만들어줍니다.
예를 들어, CatsController에서 CatsService를 사용하고 싶으면, 다음과 같이 컨트롤러 클래스를 수정합니다.
// cats.controller.ts
import { Controller, Get, Post, Body, Inject } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
@Inject()
private readonly catsService: CatsService // ????
constructor() {}
@Get()
findAll(): string[] {
return this.catsService.findAll();
}
@Post()
create(@Body() body: { name: string }): string {
const name = body.name;
this.catsService.create(name);
return `Added ${name} to the list of cats`;
}
}
이 예제에서 CatsController 생성자로 CatsService가 주입된 것을 볼 수 있고 컨트롤러 메소드인 findAll() 메소드에서 서비스의 findAll() 메소드가 호출되었습니다.
어? 그런데 코드를 보시면 생성자(constructor)에서 CatsService에 대한 인스턴스를 주입할 때 앞서 말했던 @Inject() 데코레이터를 사용하지 않고 아무것도 없는 것을 볼 수 있습니다.
이는 CatsService 클래스를 CatsModule에 등록했을 경우 해당 provider를 NestJS의 의존성 주입 시스템을 통해 자동으로 주입해주기 때문입니다. 그렇기 때문에 다음과 같이 CatsModule을 만들어주어야 합니다.
nest g mo cats
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
만약 모듈에 등록하지 않는다면 앞서 말했던 것처럼 @Inject() 데코레이터를 통해 주입해줄 수 있습니다.
// cats.controller.ts
...
@Controller('cats')
export class CatsController {
constructor(@Inject(CatsService) private readonly catsService: CatsService) {} // 수정된 부분
...
}
@Inject() 데코레이터는 명시적으로 가져올 프로바이더를 지정하기 때문에, 네스트에서 찾아온 타입을 무시하고 @Inject() 데코레이터 토큰을 사용합니다. 그러한 이유로 공식문서에서는 생성자 주입 방식을 권고하고 있습니다.
NestJS에서 의존성을 주입하는 방법
NestJS에서 의존성을 주입하는 방법은 크게 3가지가 있습니다.
- 생성자(constructor)를 이용한 방식
- 수정자(setter)를 이용한 방식
- 필드(멤버 변수 선언)를 이용한 방식
멤버 변수 필드에 변수를 선언하여 의존성을 주입할 때도 @Inject() 데코레이터를 사용할 수 있습니다.
// cats.controller.ts
...
@Controller('cats')
export class CatsController {
@Inject(CatsService)
private readonly castsService: CatsService
...
}
6. NestJS에서 미들웨어 구현하는 방법
미들웨어 함수는 요청을 가로채는데(intercept) 사용되며, 컨트롤러 핸들러에 의해 처리되기 전에 호출됩니다.
- 참고로 미들웨어는 라우팅 결정이 이루어지기 전에 실행되기 때문에 존재하지 않는 경로에 대한 요청을 처리할 수도 있는 반면, 가드, 파이프, 인터셉터는 요청의 실행 맥락에 대한 상세한 정보(ExecutionContext)를 갖기 때문에 실제로 정의된 라우트에 연결된 요청에서만 작동합니다.
미들웨어 함수는 보통 authentication(인증), logging(로깅), error handling(에러 핸들링)과 같은 작업을 위해 사용될 수 있습니다.
미들웨어 함수를 만들기 위해서, 프로젝트 폴더에서 새로운 TypeScript 파일을 생성합니다. 이 파일은 다음 세 가지 인수를 취하는 함수를 export합니다.
- request 객체, response 객체, next 함수
아래 예제는 요청 메소드와 URL을 로깅하는 간단한 미들웨어 함수입니다.
// logger.middleware.ts
import { Request, Response } from 'express';
export function loggerMiddleware(req: Request, res: Response, next: () => void) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
코드를 보시면 알겠지만 지난 포스팅에서 다룬 express의 미들웨어 사용 기법을 그대로 가져와서 사용합니다.
NestJS 애플리케이션에서 위와 같은 미들웨어 함수를 사용하기 위해서는, 미들웨어 함수를 등록하는 리스트에 해당 함수를 추가해주어야 합니다. 리스트에 추가하기 위해서는 프로젝트 폴더의 main.ts 파일을 다음과 같이 수정합니다:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { loggerMiddleware } from './logger.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(loggerMiddleware);
await app.listen(3000);
}
bootstrap();
이 예제에서 loggerMiddleware 함수는 app.use() 메소드를 통해 애플리케이션의 미들웨어 함수 리스트에 추가됩니다.
이제, 어떤 요청이 NestJS 애플리케이션에 도달하면, loggerMiddleware 함수는 컨트롤러에 의해 해당 요청이 처리되기 전에 호출될 것입니다.
7. NestJS에서 데이터베이스 연결하기
NestJS는 다양한 데이터베이스 adapter들을 이용하여 데이터베이스에 연결에 대한 내장 지원(built-in support)들을 제공합니다. 이번 예제에서, 우리는 MySQL 데이터베이스에 연결하는 TypeORM을 사용할 것입니다.
우선, 프로젝트 폴더에서 npm을 사용하여 다음 명령어를 통해 필요한 패키지를 설치합니다.
npm install --save @nestjs/typeorm typeorm mysql2
이 명령어는 TypeORM을 사용하여 MySQL 데이터베이스에 연결하는 필수 패키지를 설치할 것입니다.
.
그 다음, 프로젝트 폴더에서 새로운 TypeScript 파일을 생성하고 다음 내용을 작성합니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [
//TypeORMModule 을 동적으로 가져옴
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'test',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'], // TypeORM 이 구동될 때 인식하도록 할 entity 클래스의 경로 지정
synchronize: true, // 서비스 구동 시 소스 코드 기반으로 DB 스키마 동기화할지 여부, PROD 에서는 false 로 할 것
}),
, CatsModule
],
})
export class AppModule {}
이 예제에서는, TypeOrmModule과 CatsModule을 import 합니다. TypeOrmModule의 설정은 username과 password로 MySQL 데이터베이스에 연결하는 세부 사항을 지정합니다.
`synchronize` 옵션을 true 지정하게 되면 서비스가 실행될 때 DB 연결 시 DB가 자동으로 동기화되기 때문에 운영단계에서는 사용하지 않도록 해야 합니다.
TypeOrmModule.forRoot() 메소드는 ormconfig.json 파일을 사용하여 TypeORM 연결을 설정하는 데 사용합니다.
- ormconfig.json 방식은 typeorm 0.2.x 버전까지만 지원하기 때문에 이제는 사용되지 않습니다.
마지막으로 데이터베이스와 상호작용하기 위해 TypeORM을 사용하는 cats.service.ts 파일을 수정합니다. 아래는 구현 예시입니다:
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cat } from './cat.entity';
@Injectable()
export class CatsService {
constructor(
@InjectRepository(Cat)
private catsRepository: Repository<Cat>,
) {}
async findAll(): Promise<Cat[]> {
return this.catsRepository.find();
}
async create(cat: Cat): Promise<void> {
await this.catsRepository.save(cat);
}
}
이 예제에서, CatsService는 Cat entity의 Repository 객체를 service 생성자로 주입하는 @InjectRepository() 데코레이터를 사용하기 위해 위와 같이 수정됩니다.
이제, findAll()과 create() 메소드가 데이터베이스와 상호작용하는 catsRepository 객체를 사용할 수 있게됩니다.
8. NestJS에서 Guards와 Interceptor를 사용하는 방법
NestJS는 모듈식과 재사용가능한 방식으로 인증, 검증, 로깅과 같은 cross-cutting(교차 편집) 문제를 구현하는 한 방식으로 guard와 interceptor를 제공합니다.
Guard는 일반적으로 authorization(인증)을 구현할 때 사용되고 라우트 혹은 컨트롤러 메소드를 보호할 때 사용됩니다.
Interceptor는 들어오는 요청 혹은 나가는 응답을 수정하는데 사용되고 로깅, 속도 제한, 혹은 캐싱과 같은 작업을 할 때 사용될 수 있습니다.
Guard
우리가 새로운 Guard를 만들기 위해서, 역시나 NestJS CLI에서 제공하는 명령어를 통해 프로젝트 폴더에서 다음 명령어를 입력합니다.
nest generate guard auth
이 명령어는 auth라는 이름의 새로운 guard를 auth.guard.ts라는 이름의 파일로 생성합니다. 이 파일을 열면 다음과 같은 가장 기본의 guard 클래스를 보실 수 있을 겁니다.
// auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
Guard는 CanActivate 인터페이스를 implements하는데요. 이는 우리가 이 클래스 내부에서 canActivate() 메소드의 내용을 직접 구현해주어야 함을 의미합니다. canActivate() 메소드는 ExecutionContext 객체를 인자로 취하고 boolean 혹은 (boolean을 resolve하는) Promise 또는 Observable을 반환합니다.
이 guard가 라우트를 보호하도록 하기 위해서는, 우리가 보호하고자 하는 컨트롤러나 메서드에 @UseGaurds() 데코레이터를 붙여주면 됩니다. 예를 들어, CatsController의 create() 메소드를 보호하려면, 다음 코드처럼 컨트롤러를 수정하면 됩니다:
// cats.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './cat.entity';
import { AuthGuard } from './auth.guard';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Post()
@UseGuards(AuthGuard)
create(@Body() body: Cat): Promise<void> {
return this.catsService.create(body);
}
}
이 예제에서, @Useguards() 메소드가 create() 메소드에 붙여졌고 AuthGuard가 인자로 넘어갑니다. 이것의 의미는 create() 메소드는 AuthGuard의 canActivate() 메소드가 오직 true를 반환할 때만 호출될 수 있다는 뜻입니다.
Interceptor
새로운 interceptor를 만들기 위해서도 NestJS CLI가 제공하는 명령어를 통해 프로젝트 폴더에서 다음 명령어를 입력해 봅시다.
nest generate interceptor logging
이 명령어는 새로운 logging이라는 이름의 interceptor를 logging.interceptor.ts라는 이름의 파일을 생성할 것입니다. 이 파일을 열면 아래 코드와 같이 가장 기본 형태의 interceptor 클래스를 확인하실 수 있을 겁니다.
// logging.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
이 interceptor는 NestInterceptor를 implements하고 있는데, 이는 intercept()라는 메소드를 우리가 직접 이 클래스 내에 구현을 해주어야 한다는 것을 의미합니다. intercept() 메소드는 ExecutionContext 객체와 CallHandler 객체를 인자로 취하고 Observable을 반환합니다.
ExecutionContext 객체는 최근의 요청과 응답에 대한 정보를 포함하고 있고, CallHandler 객체는 응답에 대한 Observable을 반환하는 handle() 메소드를 포함합니다.
- 그래서 보시면 intercept() 메소드에서 return 부분에 보면 next.handle() 메소드가 사용되고 있습니다.
이러한 interceptor를 사용하려면, 중간에서 요청을 가로채기 원하는 컨트롤러나 메소드에게 @UseInterceptors() 데코레이터를 붙여주면됩니다. 예를 들어, 위의 LoggingInterceptor를 CatsController에 추가하려면 다음과 같이 컨트롤러 클래스를 수정합니다.
import { Controller, Get, Post, Body, UseGuards, UseInterceptors } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './cat.entity';
import { AuthGuard } from './auth.guard';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('cats')
@UseInterceptors(LoggingInterceptor) // Interceptor 사용
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Post()
@UseGuards(AuthGuard)
create(@Body() body: Cat): Promise<void> {
return this.catsService.create(body);
}
}
이 예제에서는 @UseInterceptors() 데코레이터가 CatsController 클래스에 붙여졌고, LoggingInterceptor가 인자로 넘겨졌습니다. 이는 CatsController로 들어오는 모든 요청에(/cats) 대해 LoggingInterceptor가 그 요청을 먼저 가로채서 자신의 로직을 먼저 처리하도록 하는 것을 의미합니다.
9. NestJS에서 테스트(Testing) 하는 방법
NestJS는 우리가 만든 애플리케이션의 컨트롤러, 서비스, 가드, 인터셉터, 그 외의 컴포넌트들에 대한 unit test(단위 테스트)를 작성할 수 있도록 하는 내장 테스팅 모듈(build-in testing module)을 제공합니다.
컨트롤러에 대한 새로운 테스트를 만들어 보기 위해서, 이 역시도 NestJS CLI에서 제공하는 명령어를 프로젝트 폴더에서 다음과 같이 입력하도록 합니다.
nest generate test cats
이 명령어는 cats.controller.spec.ts라는 이름의 새로운 테스트 파일을 src/ 폴더 아래에 생성할 것입니다. 이 파일을 열면 아래 코드와 같이 작성된 가장 기본적인 테스트의 구현을 보실 수 있습니다.
import { Test, TestingModule } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let controller: CatsController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
controller = app.get<CatsController>(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['Tom', 'Garfield', 'Sylvester'];
jest.spyOn(controller, 'findAll').mockImplementation(() => result);
expect(await controller.findAll()).toBe(result);
});
});
});
이 테스트 방식은 NestJS의 테스팅 모듈을 통해 CatsController와 CatsService의 새로운 인스턴스를 생성하고 해당 컨트롤러의 findAll() 메소드를 테스트합니다.
이 테스트를 실행해보려면, 프로젝트 폴더에서 다음 명령어를 입력합니다.
npm run test
이 명령어는 src/ 폴더 내부의 모든 테스트를 실행하고 콘솔에 그 결과를 출력합니다.
이와 같이 우리는 서비스, 가드, 인터셉터, 그 외의 컴포넌트들에 대해서도 똑같이 테스트 코드를 작성할 수 있습니다.
10. NestJS에서 Plugin과 Module을 확장하는 법
NestJS는 우리의 애플리케이션 기능을 플러그인과 모듈을 사용하여 확장할 수 있도록 해줍니다.
Plugin은 우리의 애플리케이션으로 쉽게 통합될 수 있는 재사용가능한 기능의 조각인 반면, Module은 논리 단위(logical unit)로 우리의 애플리케이션을 조직하는데 사용될 수 있는 컴포넌트의 모음입니다.
Plugin
새로운 plugin을 만들기 위해서는, 프로젝트 폴더에서 어떤 클래스나 함수를 export하는 새로운 TypeScript 파일을 생성해줍니다. 예를 들어, 모든 요청과 응답에 대해 로그를 찍는 logger plugin을 생성하려면 logger.plugin.ts 파일을 생성하여 그 내용을 아래와 같이 작성합니다.
import { Injectable } from '@nestjs/common';
@Injectable()
export class LoggerPlugin {
onRequest(req, res) {
console.log(`${req.method} ${req.originalUrl}`);
}
onResponse(req, res) {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode}`);
}
}
이 클래스는 요청 메소드와 요청 URL에 대한 로그를 찍는 onRequest() 메소드와 응답 메소드와 응답 URL, 상태 코드를 찍는 onResponse() 메소드를 정의합니다.
우리의 애플리케이션에서 이 플러그인을 사용하려면 먼저 LoggerPlugin의 새로운 인스턴스를 생성해서 app.use() 메소드를 사용하여 미들웨어 함수 리스트에 해당 인스턴스를 추가해줍니다. 예를 들어, 프로젝트 폴더의 main.ts 파일을 다음과 같이 수정할 수 있습니다:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggerPlugin } from './logger.plugin';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use((req, res, next) => {
const logger = new LoggerPlugin();
logger.onRequest(req, res);
res.on('finish', () => logger.onResponse(req, res));
next();
});
await app.listen(3000);
}
bootstrap();
이 예제에서, LoggerPlugin는 애플리케이션의 미들웨어로 사용됩니다. onRequest() 메소드는 애플리케이션에 요청이 도달할 때 호출되고 onResponse() 메소드는 응답이 다시 클라이언트에게 전달될 때 호출됩니다.
Module
새로운 module을 만들기 위해서, NestJS CLI가 제공하는 다음 명령어를 프로젝트 폴더에서 입력합니다.
nest generate module cats
이 명령어는 CatsModule이라는 이름의 새로운 module을 cats.module.ts라는 파일에 생성할 것입니다. 이 파일을 열면 다음 코드와 같은 가장 기본 형태의 module 클래스를 보실 수 있을 겁니다.
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
이 module는 CatsController와 'cats'와 관련된 비지니스 로직을 구현하고 요청을 처리하는데 사용되는 CatsService를 정의합니다.
애플리케이션에서 이 module을 사용하려면 AppModule로 이 module을 import 시키고 imports 배열에 이 CatsModule을 추가해주어야 합니다. 예를 들어, 프로젝트 폴더의 app.module.ts 파일을 다음과 같이 수정할 수 있습니다:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CatsModule } from './cats/cats.module';
import { Cat } from './cats/cat.entity';
@Module({
imports: [TypeOrmModule.forRoot(), CatsModule],
providers: [],
})
export class AppModule {}
'Back-end > NestJS' 카테고리의 다른 글
[NestJS] NestJS CLI로 REST API를 사용한 CRUD 기능 만들기(5분버전 vs. 심화버전) with TypeORM & MySQL (0) | 2024.01.24 |
---|---|
[NestJS] NestJS에서 Swagger 사용법 (feat. API Documentation) (2) | 2024.01.23 |
[NestJS] NestJS를 위한 선수지식 Node.js & Express.js 이해 (feat. Logging, 폴더 구조) (0) | 2024.01.21 |
[NestJS] NestJS와 관련된 기술 용어 정리 (DI, IoC, AOP 등...) (0) | 2024.01.19 |
[NestJS] NestJS 시작 전에 알아야 하는 백엔드 지식 (0) | 2024.01.10 |
당신이 좋아할만한 콘텐츠
-
[NestJS] NestJS CLI로 REST API를 사용한 CRUD 기능 만들기(5분버전 vs. 심화버전) with TypeORM & MySQL 2024.01.24
-
[NestJS] NestJS에서 Swagger 사용법 (feat. API Documentation) 2024.01.23
-
[NestJS] NestJS를 위한 선수지식 Node.js & Express.js 이해 (feat. Logging, 폴더 구조) 2024.01.21
-
[NestJS] NestJS와 관련된 기술 용어 정리 (DI, IoC, AOP 등...) 2024.01.19
소중한 공감 감사합니다