서문 및 소개

들어가기에 앞서

이 문서는 Nest.js의 공식문서를 바탕으로 작성했습니다. 따라서 이 문서는 전반적인 흐름은 공식 문서와 비슷하며 설명도 많은 부분이 공식 문서를 바탕으로 합니다. 다만 공식 문서 자체가 초보자들을 위한 문서는 아니고 좀 생략된 부분도 많다보니 초보자들이 읽고 이해하기가 쉽지 않습니다. 따라서 흐름만 참고는 하는데 중간중간에 제가 난입해서 설명하는 부분도 있고, 원문 자체가 잘 이해하기 힘들게 쓰여있으면 아예 이해가도록 썼습니다. 회사 내부 주니어 개발자 개발용으로 만든겁니다.

이 글을 읽기 전에

기본적인 Express 지식과 타입스크립트 지식이 있다고 가정합니다. Express는 워낙 책과 정보가 많아서 소개를 생략하고, 타입스크립트 지식이 없으신 분들은 타입스크립트 핸드북(영어)이나 번역된 문서를 참고해주시기 바랍니다.

또한 이 글은 Nest의 공식문서 번역이 아닙니다. 물론 공식 문서를 바탕으로 제멋대로 쓴 글입니다. 또한 클라우드 환경에서 쓴다는 가정하에 File Upload, Queue 등은 생략하였습니다.

그리고 시간날 때마다 쓰고 있으니까 빨리 써달라고 보채지 말아주시기 바랍니다 ㅠㅠ

소개

Nest(NestJS)는 효율적이고 확장 가능한 Node.js 서버 사이드 애플리케이션을 구축하기 위한 프레임워크입니다. 대부분의 Nest의 코드는 타입스크립트로 만들어졌으며 타입스크립트를 완전하게 지원합니다. 하지만 Nest는 Express와 동급은 아닙니다. Nest는 HTTP 요청을 다루는 웹서버로 내부적으로 Express를 사용하고 있습니다. 즉 Nest는 Express 위에서 돌아가는 레이어에 불과합니다.

아! 참고로 Express가 기본 웹서버지만 Express 대신 Fastify를 사용하도록 구성 할 수도 있습니다.

그럼 왜 Nest?

Nest가 각광받는 이유는 여러 가지가 있습니다. 제가 생각하기로는 3가지로 요약되는데요.

첫 번째, 어플리케이션이 점점 커지고 복잡해지면 아키텍쳐건 어플리케이션이건 확장이 용이하고 느슨하게 결합된 형태로 발전합니다. 이는 Nest가 지향하는 아키텍쳐입니다.

두 번째, Express/Fastify 위에서 동작하고, 추상화된 API를 제공하지만 완전하게 Express를 추상화하고 캡슐화하지 않았기 때문에 기존 Express에서 동작하는 수 많은 라이브러리를 그대로 사용할 수 있습니다.

세 번째, 구조를 강제합니다. 이는 대규모 팀 협업에도 좋고, 신규 개발자가 들어왔을때 적응에 도움이 됩니다. 마개조 수준으로 Express를 자신만의 방식으로 사용하는 회사들도 많이 있지만, 보통 문서화에 문제가 있거나 추상화 수준이 좋지 못한 경우가 많이 있습니다.

설치

시작하려면 Nest CLI를 사용하여 프로젝트를 만들거나 스타터 프로젝트(보일러 플레이트)를 클론하여 만들 수 있습니다. 물론 둘 다 같은 결과입니다. Nest CLI로 프로젝트를 만드려면 아래와 같이 하시면 됩니다.

$ npm i -g @nestjs/cli
$ nest new project-name

이렇게하면 새 프로젝트 디렉터리가 생성되며 핵심적이고 기본적인 초기 Nest 파일 및 지원 모듈이 생성됩니다. 처음 사용하는 사용자는 Nest CLI로 새 프로젝트를 만들기를 추천합니다.

또 다른 방법은 Git을 사용하여 타입스크립트 스타터 프로젝트를 설치할 수 있습니다.

$ git clone https://github.com/nestjs/typescript-starter.git project
$ cd project
$ npm install
$ npm run start

브라우저를 열고 http://localhost:3000/ 로 이동합니다.

다른 방법도 있습니다. 그리 추천하는 방법은 아닌데, npm/yarn을 사용하여 하나씩 하나씩 설치하는 방법입니다. 이 경우 보일러플레이트 코드를 직접 생성해야 합니다.

$ npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata

첫 단계

CRUD 기능이 있는 어플리케이션을 같이 만들어보면서 Nest의 핵심 개념에 대해 이해하도록 하겠습니다.

언어

Nest는 타입스크립트를 우선적으로 지원하지만 순수 자바스크립트를 사용할 수 있습니다. Nest는 최신 언어 기능을 활용하므로 바닐라 자바스크립트 와 함께 사용하려면 Babel 컴파일러가 필요합니다.

공식 문서에서는 대부분 Typescript로 예제가 작성되어 있지만, 바닐라 자바스크립트 코드도 같이 제공하고 있습니다. (각 코드 스니펫의 오른쪽 상단에 있는 언어 버튼을 클릭). 하지만 저는 지금 글을 쓰는 도구의 한계로 Typescript로만 예제를 지원하도록 하겠습니다.

필요사항

  • Node.js (> = 10.13.0, v13 제외)

설치

맨 첫 장에서 설명한대로 스켈레톤 프로젝트를 만드는 방법에는 3가지가 있는데, 그 중 가장 쉽고 널리 쓰이는 Nest CLI로 프로젝트를 만들어 보겠습니다. Nest CLI를 설치하고 새로운 프로젝트를 만든다는 명령어를 입력하면 됩니다.

$ npm i -g @nestjs/cli
$ nest new project-name

패키지 매니저를 npm으로 사용할건지 yarn으로 사용할건지 물어보고, 몇 가지 파일을 생성하고 필요한 노드 모듈을 설치합니다. 만들어진 프로젝트 루트로 가보겠습니다. 아래는 맥에서 ls로 파일 내역을 확인한 화면입니다.

-rw-r--r--    1   staff   3.3K  5  8 10:36 README.md
-rw-r--r--    1   staff    64B  5  8 10:36 nest-cli.json
drwxr-xr-x  605   staff    19K  5  8 10:37 node_modules
-rw-r--r--    1   staff   710K  5  8 10:37 package-lock.json
-rw-r--r--    1   staff   1.9K  5  8 10:36 package.json
drwxr-xr-x    7   staff   224B  5  8 10:36 src
drwxr-xr-x    4   staff   128B  5  8 10:36 test
-rw-r--r--    1   staff    97B  5  8 10:36 tsconfig.build.json
-rw-r--r--    1   staff   339B  5  8 10:36 tsconfig.json

생성된 디렉토리와 파일에 대해서 설명해보겠습니다.

이름설명
README.mdREADME 파일
node_modules앱에 필요한 라이브러리 설치 디렉토리
package-lock.json설명 생략
package.json설명 생략
src소스파일
testE2E(End-to-End Testing) 디렉토리
tsconfig.build.json빌드할때 필요한 타입스크립트 설정
tsconfig.json타입스크립트 설정

중요한건 src 디렉토리입니다. src 디렉토리를 살펴보겠습니다.

이름설명
app.controller.ts하나의 경로(route)만 있는 컨트롤러
app.controller.spec.ts컨트롤러 단위 테스트
app.module.ts어플리케이션 루트 모듈
app.service.ts하나의 메소드만 있는 서비스
main.tsNestFactory를 사용해서 Nest 어플리케이션 인스턴스를 생성하는 어플리케이션의 시작점

여기에 모르는 단어가 많이 나옵니다. 컨트롤러, 서비스, 모듈 입니다. 이건 천천히 알아가도록 하겠습니다.

그럼 이 어플리케이션의 시작점인 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();

코드는 굉장히 간단합니다. bootstrap이라는 함수를 정의하고 이를 실행하고 끝납니다. 상식으로 bootstrap이라는 단어는 컴퓨터 분야에서 보통 한 번 시작하면 다른 외부의 도움없이 스스로 진행하는 행위를 말합니다. 상세한 설명은 여기를 참고하세요. 다시 코드로 돌아와서 이 함수 내에서는 NestFactory를 사용하여 어플리케이션 인스턴스를 생성합니다. NestFactory는 Nest 응용 프로그램을 만들수 있는 몇 가지 정적 메소드를 제공하는데, create()메소드는 INestApplication 인터페이스를 충족하는 응용 프로그램 객체를 반환합니다. 이 객체는 위 코드처럼 3000번 포트를 열고 인바운드 HTTP 요청을 기다리며, 요청이 오면 이를 처리하고 응답을 보내는 웹서버의 역할을 합니다.

플랫폼

Nest는 플랫폼에 구애받지 않은 프레임워크를 목표로 합니다. 플랫폼을 독립적으로 만들었기 때문에 이를 통해 개발자가 여러 유형의 응용 프로그램에서 활용할 수 있는 재사용 가능한 논리적 부분을 만들 수 있습니다. 기술적으로 Nest는 어댑터가 생성되면 모든 Node HTTP 프레임워크와 함께 사용할 수 있습니다. Node 기반 HTTP 프레임워크는 크게 Express, Koa, Fastify가 있지만 Nest는 기본적으로 Express와 Fastify를 지원합니다. 이 중 적절한 걸 선택하면 됩니다.

platform-express

두 말할 나위없는 가장 유명한 Node기반 웹 프레임워크입니다. 많은 사용자가 있으며, 수 많은 프로덕션에서 그 성능과 안정성을 검증받은 프레임워크입니다. Nest에서는 기본으로 Express를 사용하기 때문에 추가 설정이 필요없습니다.

platform-fastify

속도와 효율성을 중시하는 웹 프레임워크입니다. Express대비 거의 2배의 속도와 처리량, 그리고 짧은 지연속도(Latency)를 자랑합니다.

express, fastify 중 어떤 플랫폼을 선택해도 그 플랫폼 API가 아닌 Nest에서 정의한 어플리케이션 인터페이스를 노출합니다. Express는 NestExpressApplication가 되며, Fastify는 NestFastifyApplication이 됩니다.

전전 단락에서 "INestApplication 인터페이스를 충족하는 응용 프로그램 객체"가 바로 NestExpressApplicationNestFastifyApplication입니다. 이 두 개 모두 INestApplication을 상속하기 때문입니다. 아래는 실제 정의한 코드입니다.

export interface NestExpressApplication extends INestApplication {
  // ...
}
export interface NestFastifyApplication extends INestApplication {
  // ...
}

NestFactory.create()에 아래와 같이 메소드에 유형을 전달 하면 app객체는 해당 특정 플랫폼에서만 사용할 수 있는 메소드를 갖게됩니다.

const app = await NestFactory.create<NestExpressApplication>(AppModule);

그러나 실제로 기본 플랫폼 API를 사용하려는 경우가 아니면 굳이 유형을 지정할 필요가 없습니다. X랄같은 버그를 만나지 않는다면 Nest에서 제공하는 API 쓰기도 바쁩니다.

응용 프로그램 실행

Nest CLI로 설치가 완료되면 OS 명령 프롬프트에서 다음 명령을 실행하여 인바운드 HTTP 요청을 수신하는 애플리케이션을 시작할 수 있습니다.

$ npm run start

이 명령은 src/main.ts 파일에 정의된 포트에서 수신 대기하는 HTTP 서버로 앱을 시작 합니다. 기본은 3000번 포트로, 애플리케이션이 실행되면 브라우저 주소창에 http://localhost:3000/ 를 입력해봅시다. Hello World! 메시지가 표시되면 잘 동작하고 있는겁니다.

첫 단계

CRUD 기능이 있는 어플리케이션을 같이 만들어보면서 Nest의 핵심 개념에 대해 이해하도록 하겠습니다.

언어

Nest는 타입스크립트를 우선적으로 지원하지만 순수 자바스크립트를 사용할 수 있습니다. Nest는 최신 언어 기능을 활용하므로 바닐라 자바스크립트 와 함께 사용하려면 Babel 컴파일러가 필요합니다.

공식 문서에서는 대부분 Typescript로 예제가 작성되어 있지만, 바닐라 자바스크립트 코드도 같이 제공하고 있습니다. (각 코드 스니펫의 오른쪽 상단에 있는 언어 버튼을 클릭). 하지만 저는 지금 글을 쓰는 도구의 한계로 Typescript로만 예제를 지원하도록 하겠습니다.

필요사항

  • Node.js (> = 10.13.0, v13 제외)

설치

맨 첫 장에서 설명한대로 스켈레톤 프로젝트를 만드는 방법에는 3가지가 있는데, 그 중 가장 쉽고 널리 쓰이는 Nest CLI로 프로젝트를 만들어 보겠습니다. Nest CLI를 설치하고 새로운 프로젝트를 만든다는 명령어를 입력하면 됩니다.

$ npm i -g @nestjs/cli
$ nest new project-name

패키지 매니저를 npm으로 사용할건지 yarn으로 사용할건지 물어보고, 몇 가지 파일을 생성하고 필요한 노드 모듈을 설치합니다. 만들어진 프로젝트 루트로 가보겠습니다. 아래는 맥에서 ls로 파일 내역을 확인한 화면입니다.

-rw-r--r--    1   staff   3.3K  5  8 10:36 README.md
-rw-r--r--    1   staff    64B  5  8 10:36 nest-cli.json
drwxr-xr-x  605   staff    19K  5  8 10:37 node_modules
-rw-r--r--    1   staff   710K  5  8 10:37 package-lock.json
-rw-r--r--    1   staff   1.9K  5  8 10:36 package.json
drwxr-xr-x    7   staff   224B  5  8 10:36 src
drwxr-xr-x    4   staff   128B  5  8 10:36 test
-rw-r--r--    1   staff    97B  5  8 10:36 tsconfig.build.json
-rw-r--r--    1   staff   339B  5  8 10:36 tsconfig.json

생성된 디렉토리와 파일에 대해서 설명해보겠습니다.

이름설명
README.mdREADME 파일
node_modules앱에 필요한 라이브러리 설치 디렉토리
package-lock.json설명 생략
package.json설명 생략
src소스파일
testE2E(End-to-End Testing) 디렉토리
tsconfig.build.json빌드할때 필요한 타입스크립트 설정
tsconfig.json타입스크립트 설정

중요한건 src 디렉토리입니다. src 디렉토리를 살펴보겠습니다.

이름설명
app.controller.ts하나의 경로(route)만 있는 컨트롤러
app.controller.spec.ts컨트롤러 단위 테스트
app.module.ts어플리케이션 루트 모듈
app.service.ts하나의 메소드만 있는 서비스
main.tsNestFactory를 사용해서 Nest 어플리케이션 인스턴스를 생성하는 어플리케이션의 시작점

여기에 모르는 단어가 많이 나옵니다. 컨트롤러, 서비스, 모듈 입니다. 이건 천천히 알아가도록 하겠습니다.

그럼 이 어플리케이션의 시작점인 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();

코드는 굉장히 간단합니다. bootstrap이라는 함수를 정의하고 이를 실행하고 끝납니다. 상식으로 bootstrap이라는 단어는 컴퓨터 분야에서 보통 한 번 시작하면 다른 외부의 도움없이 스스로 진행하는 행위를 말합니다. 상세한 설명은 여기를 참고하세요. 다시 코드로 돌아와서 이 함수 내에서는 NestFactory를 사용하여 어플리케이션 인스턴스를 생성합니다. NestFactory는 Nest 응용 프로그램을 만들수 있는 몇 가지 정적 메소드를 제공하는데, create()메소드는 INestApplication 인터페이스를 충족하는 응용 프로그램 객체를 반환합니다. 이 객체는 위 코드처럼 3000번 포트를 열고 인바운드 HTTP 요청을 기다리며, 요청이 오면 이를 처리하고 응답을 보내는 웹서버의 역할을 합니다.

플랫폼

Nest는 플랫폼에 구애받지 않은 프레임워크를 목표로 합니다. 플랫폼을 독립적으로 만들었기 때문에 이를 통해 개발자가 여러 유형의 응용 프로그램에서 활용할 수 있는 재사용 가능한 논리적 부분을 만들 수 있습니다. 기술적으로 Nest는 어댑터가 생성되면 모든 Node HTTP 프레임워크와 함께 사용할 수 있습니다. Node 기반 HTTP 프레임워크는 크게 Express, Koa, Fastify가 있지만 Nest는 기본적으로 Express와 Fastify를 지원합니다. 이 중 적절한 걸 선택하면 됩니다.

platform-express

두 말할 나위없는 가장 유명한 Node기반 웹 프레임워크입니다. 많은 사용자가 있으며, 수 많은 프로덕션에서 그 성능과 안정성을 검증받은 프레임워크입니다. Nest에서는 기본으로 Express를 사용하기 때문에 추가 설정이 필요없습니다.

platform-fastify

속도와 효율성을 중시하는 웹 프레임워크입니다. Express대비 거의 2배의 속도와 처리량, 그리고 짧은 지연속도(Latency)를 자랑합니다.

express, fastify 중 어떤 플랫폼을 선택해도 그 플랫폼 API가 아닌 Nest에서 정의한 어플리케이션 인터페이스를 노출합니다. Express는 NestExpressApplication가 되며, Fastify는 NestFastifyApplication이 됩니다.

전전 단락에서 "INestApplication 인터페이스를 충족하는 응용 프로그램 객체"가 바로 NestExpressApplicationNestFastifyApplication입니다. 이 두 개 모두 INestApplication을 상속하기 때문입니다. 아래는 실제 정의한 코드입니다.

export interface NestExpressApplication extends INestApplication {
  // ...
}
export interface NestFastifyApplication extends INestApplication {
  // ...
}

NestFactory.create()에 아래와 같이 메소드에 유형을 전달 하면 app객체는 해당 특정 플랫폼에서만 사용할 수 있는 메소드를 갖게됩니다.

const app = await NestFactory.create<NestExpressApplication>(AppModule);

그러나 실제로 기본 플랫폼 API를 사용하려는 경우가 아니면 굳이 유형을 지정할 필요가 없습니다. X랄같은 버그를 만나지 않는다면 Nest에서 제공하는 API 쓰기도 바쁩니다.

응용 프로그램 실행

Nest CLI로 설치가 완료되면 OS 명령 프롬프트에서 다음 명령을 실행하여 인바운드 HTTP 요청을 수신하는 애플리케이션을 시작할 수 있습니다.

$ npm run start

이 명령은 src/main.ts 파일에 정의된 포트에서 수신 대기하는 HTTP 서버로 앱을 시작 합니다. 기본은 3000번 포트로, 애플리케이션이 실행되면 브라우저 주소창에 http://localhost:3000/ 를 입력해봅시다. Hello World! 메시지가 표시되면 잘 동작하고 있는겁니다.

컨트롤러 1편

웹 어플리케이션에서 컨트롤러(Controller)란 외부의 요청을 처리하는 모듈을 의미합니다. 좀 더 정확하게 말하면 하나 이상의 클라이언트가 보내는 요청을 처리하고 요청을 보낸 클라이언트에게 응답을 반환하는 역할이죠.

컨트롤러

컨트롤러의 목적은 애플리케이션에 대한 특정 요청을 수신하는 것입니다. 라우팅 메커니즘은 어떤 컨트롤러가 해당 요청을 처리할지 조정합니다. 보통은 각각의 컨트롤러는 하나 이상의 경로가 있으며, 각기 다른 경로는 각기 다른 행동을 수행합니다.

라우팅(Routing)이란?

라우팅은 네트워크에서는 어떠한 패킷을 원하는 곳으로 보내는 행위를 가리킵니다.(여기를 참고하세요). 이 글에서 라우팅이란 들어온 HTTP 요청을 특정 컨트롤러로 보내는 행위를 말합니다.

Nest는 기본 컨트롤러를 생성하기 위해서 클래스와 데코레이터를 사용합니다. 데코레이터는 클래스를 필수 메타데이터와 연결하고 Nest가 라우팅 맵을 만들 수 있도록 합니다.

라우팅

다음 예제에서는 기본 컨트롤러를 정의하는 데 필요한 @Controller() 데코레이터를 사용합니다. 예제에서는 선택적 경로 경로 접두사로 cats를 명시했습니다.

// cats.controller.ts / 예제1

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

데코레이터에서 경로 접두사를 사용하면 관련 경로 집합을 쉽게 그룹화하고 반복 코드를 최소화 할 수 있습니다. 예를 들어 볼까요? 위 예제의 findAll()Get에도 경로를 직접 입력할 수 있습니다. Get은 이 메소드가 GET 메소드를 처리한다는 말인데 곧 이어 설명합니다. @Controller() 데코레이터의 선택적 경로 접두어를 안붙인다면 아래와 같이 모든 메소드에 경로를 붙여줘야 합니다.

// 예제2
import { Controller, Get, Param } from '@nestjs/common';

@Controller()
export class CatsController {
  @Get('cats/all')
  findAll(): string {
    return 'This action returns all cats';
  }

  @Get('cats/:id')
  findOne(@Param('id') id: number): string {
    return 'This action returns one cats';
  }

  @Get('cats/musical')
  findMusical() {
    return 'This action returns musical CATs';
  }
}

cats/ 단어의 반복이 계속 됩니다. 위 코드를 아래와 같이 바꾸는게 @Controller()데코레이터의 경로 접두어입니다.

// 예제3
import { Controller, Get, Param } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get('all')
  findAll(): string {
    return 'This action returns all cats';
  }

  @Get(':id')
  findOne(@Param('id') id: number): string {
    return 'This action returns one cats';
  }

  @Get('musical')
  findMusical() {
    return 'This action returns musical CATs';
  }
}

다시 예제1로 돌아와서 findAll() 메서드 앞의 @Get() 데코레이터가 있습니다. @Get() 데코레이터는 Nest 프레임워크에 해당 메서드가 HTTP 요청의 특정 엔드포인트를 처리한다고 알려줍니다. 엔드포인트는 HTTP 요청 메서드(여기서는 GET) 및 라우트 패스(Route path)에 해당합니다. 라우트도 경로고 패스도 경로라는 뜻이라서 일부러 원문으로 번안한 Nest에서 말하는 라우트 패스란 findAll()과 같은 메서드의 URL 경로는 컨트롤러가 선언한 접두사(선택사항)와 @Get()과 같은 요청 데코레이터에 지정된 경로를 연결하여 정해진다는 점입니다. 예를 들어 예제1에서는 컨트롤러의 패스가 cats이며, findAll()은 경로 정보가 없기 때문에 Nest는 GET /cats 요청을 findAll()메서드에 매핑합니다. 예제3에서는 /cats/musical URL 요청이 오면 findMusical()메서드가 실행됩니다.

API 엔드포인트(Endpoint)란?

웹서비스에서 API 엔드포인트란 클라이언트가 여러분의 API에 접근할 수 있는 URL을 뜻합니다.

위 예제에서는 클라이언트에서 /cats요청이 왔을때 findAll()메서드로 라우팅하는데, Nest 입장에서는 이 findAll이라는 메서드 이름은 별로 중요하지 않습니다. 반드시 요청 데코레이터 다음에는 메서드를 선언해야 하지만 이름은 아무거나 지어도 됩니다. 심지어 abcdef같은 메서드 이름을 지어도 됩니다. 왜냐하면 Nest는 이런 메서드 이름에 어떤 의미도 부여하지 않기 때문이죠.

클라이언트의 응답을 처리하는 메서드는 HTTP 상태코드와 응답을 반환합니다. 위의 findAll()은 상태코드 200과 단순한 문자열을 반환할 뿐이죠. 왜 그럴까요? 이를 설명하기 위해 먼저 Nest가 응답을 조작하기 위해 두 가지 다른 옵션을 사용한다는 개념을 소개하겠습니다.

표준 (권장)

내장 메서드를 사용하면 요청 핸들러(여기서는 findAll()과 같은 요청을 처리하는 메서드)가 JavaScript 객체 또는 배열을 반환 할 때 자동으로 JSON으로 직렬화(Serialize)됩니다. 만약 Javascript 원시형(Primitive Type. 예를 들어 string, number, boolean이 있습니다)을 반환할 경우 직렬화하지 않고 바로 그 값을 전송합니다. 이렇게하면 응답 처리가 간단해집니다. 값을 반환하기만 하면 Nest가 나머지를 처리합니다.

또한 응답의 상태 코드는 201을 사용하는 POST 요청을 제외하고는 항상 기본적으로 200입니다. 나는 특별히 200 외에 다른 값(예: 204)을 보내고 싶다면 @HttpCode(...) 데코레이터를 사용해서 핸들러 수준에서 동작을 쉽게 변경할 수 있습니다.

특정 라이브러리 전용

Express나 Fastify 별로 직접 응답을 조작할 수 있습니다. findAll(@Res() response))과 같이 @Res() 데코레이터를 메서드 시그니쳐에 추가하면 됩니다. 이렇게 접근하면 Express/Fastify의 기본 Response 메서드를 사용할 수 있습니다. 예를 들어 Express를 사용한다면 response.status(200).send() 이렇게 200을 보낼 수가 있다는 말이죠.

요청 객체(Request Object)

방금 Response를 설명했으니 Request 객체에 대해서 언급하고 2편을 마무리하겠습니다. 핸들러(예: findAll()는 종종 클라이언트가 보낸 요청(Request)의 세부 정보를 필요로 할 때가 있습니다. 예를 들어 세션 정보나 헤더 정보가 필요할때 말이죠. Nest는 Express나 Fastify의 요청 객체에 접근할 수 있도록 합니다. @Res()처럼 핸들러의 시그니쳐에 데코레이터를 추가하여 Nest에 주입하도록 지시하여 요청 객체에 액세스할 수 있습니다.

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

타입스크립트를 사용할 경우, 위의 예제처럼 express의 Request 타이핑을 위해서는 @types/express 패키지를 설치하시기 바랍니다.

요청 객체는 HTTP 요청을 뜻합니다. 이 요청에는 쿼리 문자열, 매개 변수, HTTP 헤더 및 본문에 대한 속성을 가지고 있습니다 (자세한 내용은 여기). Nest에는 이 속성을 편리하게 가져올 수 있도록 편리하게 사용할 수있는 @Body()또는 @Query() 같은 전용 데코레이터를 제공합니다. 아래는 제공된 데코레이터와 데코레이터가 의미하는 원래 플랫폼(Express, Fastify)의 객체 목록입니다.

@Request(), @Req()req
@Response(), @Res() *res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

* 기본 HTTP 플랫폼 (예 : Express 및 Fastify)에서 입력과의 호환성을 위해 Nest는 @Res()및 @Response()데코레이터를 제공합니다. @Res()는 단순히 @Response()의 별칭입니다. 둘 다 기본 HTTP 플랫폼 response 객체의 인터페이스를 직접 노출합니다. 하지만 개인적인 경험으로는 @Response()보다는 @Res()를 사용하시기 바랍니다. response객체를 완전하게 활용하기 위해서는 기본 HTTP 플랫폼의 response의 타이핑을 가져와야하는데 아래와 같이 @Response()와 기본 HTTP 플랫폼의 response와 이름이 겹치기 때문입니다.

import { Controller, Get, Response } from '@nestjs/common';
import { Response } from 'express'; // 이 부분에서 에러

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Response() res: Response) {}
}

// 아니면

import { Controller, Get, Response } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Response() res: any) {}
}

만약 메서드 핸들러에서 @Res()@Respoonse()를 사용했다면, 위에서 언급한대로 Nest를 특정 라이브러리 전용 모드로 사용한다는 것을 뜻하기 때문에 반드시 응답을 관리해야 합니다. 이 경우 res.json(...)res.send(...)로 직접적으로 응답을 주지 않으면 HTTP 서버가 중단되는 참극을 목격하실 수 있습니다.

원 글에서는 컨트롤러 설명을 1편의 글로 적었지만, 저는 두 편으로 쪼개어 설명하겠습니다.

컨트롤러 2편

자원

이전에는 cats 리소스 (GET 경로)를 가져오는 엔드 포인트를 정의했습니다. 여기에 더해 새로운 레코드를 생성하는 엔드 포인트도 제공하려고 합니다. 이를 위해 POST 핸들러를 생성 해 보겠습니다.

// cats.controller.ts

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

간단하네요. Nest는 @Post()외에도 모든 표준 HTTP 메서드에 대한 데코레이터를 제공합니다. @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head()가 있습니다. 추가적으로 @All()과 같이 모든 항목을 처리하는 엔드 포인트도 있습니다.

와일드 카드 라우팅

패턴 기반 경로도 지원됩니다. 예를 들어 별표는 와일드 카드로 사용되며 모든 문자 조합과 일치합니다.

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

abcd, ab_cd, abecd 경로는 'ab\*cd'경로 경로가 일치합니다. ?, +, *, 및 () 문자는 경로 정보에 사용될 수 있습니다. 얼핏보면 정규 표현식과 같다고 생각되겠지만 정규 표현식의 부분집합입니다. 정규 표현식과는 달리 하이픈(-)과 점(.)은 문자 그대로 경로로 인식합니다.

상태코드

언급했듯이 응답 상태코드 는 201POST 요청을 제외하고 기본적으로 항상 200 입니다. 핸들러 레벨에서 @HttpCode(...)데코레이터를 추가하여 이 동작을 쉽게 변경할 수 있습니다. HttpCode@nestjs/common에서 Import 할 수 있습니다. 아래 코드는 HTTP 상태코드 204를 반환합니다.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

가끔씩 상태코드는 고정되지 않고 여러 요인에 따라 변하기도 합니다. 이 경우 주입된 @Res() 객체를 사용하여 직접 상태코드와 응답을 반환하시기 바랍니다.

헤더

사용자 지정 응답 헤더를 지정하려면 @Header() 데코레이터 혹은 라이브러리 특정 응답 객체를 사용할 수 있습니다. 예를 들어 res.header() 말이죠. Header@nestjs/common에서 import 할 수 있습니다.

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

리디렉션

응답을 특정 URL로 리디렉션하려면 @Redirect() 데코레이터 또는 라이브러리 특정 응답 객체를 사용합니다. 예를 들어 res.redirect()를 직접 호출 할 수 있습니다.

@Redirect()는 2개의 인수를 받습니다. url, statusCode 인데 모두 필수는 아닙니다. 생략 된 경우 statusCode의 기본 값은 302(Found) 입니다.

@Get()
@Redirect('https://nestjs.com', 301)

HTTP 상태 코드나 리디렉션 URL을 동적으로 확인해야하는 경우가 있습니다. 다음과 같은 형태로 경로 핸들러 메서드에서 객체를 반환하면됩니다.

{
  "url": string,
  "statusCode": number
}

반환 된 값은 @Redirect()데코레이터에 전달 된 모든 인수를 재정의합니다. 예를 들면

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
}
}

경로 매개변수

만약 여러분이 요청의 일부로 동적인 데이터를 받아야 할 경우(예: /GET /cats/11과 같은 정보)에는 정적인 경로로는 어떻게 할 수 없죠. 매개 변수가 있는 경로를 사용하기 위해서는 경로에 매개 변수 토큰을 추가하여 요청 URL의 해당 위치에서 동적 값을 잡아낼 수 있습니다. 아래 예제에서는 @Get() 데코레이터에서 경로 매개 변수 토큰을 어떻게 사용하는지 보여줍니다. 이런 방식으로 선언된 경로 매개 변수는 @Param() 데코레이터를 사용하여 접근할 수 있으며, 이는 메서드 서명에 추가되어야 합니다.

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param() 데코레이터는 경로 매개 변수에 접근할 때 사용합니다. 위의 코드에서 볼 수 있듯이 params.id로 경로 매개변수 id를 참조할 수 있습니다. 아래의 예제처럼 특정 매개 변수 토큰을 데코레이터에 전달한 다음 메서드 본문에서 이름으로 바로 경로 매개 변수를 참조할 수도 있습니다. 경로 매개변수가 하나 있을 경우 저는 보통 아래와 같이 사용합니다.

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

Param@nestjs/common 패키지에서 import 할 수 있습니다.

하위 도메인 라우팅

@Controller 데코레이터는 host 옵션을 사용하여 들어오는 요청의 HTTP 호스트가 특정 값과 일치해야지 처리하는 옵션을 추가할 수 있습니다.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

경로 매개변수처럼 host 옵션도 토큰을 사용하여 호스트 이름의 특정 위치에서 동적 값을 가져올 수 있습니다. 아래 예제의 @Controller() 데코레이터 내에 설정한 호스트 매개 변수 토큰을 참고하시기 바랍니다. 이러한 방식으로 선언한 호스트 매개 변수는 각 핸들러의 @HostParam() 데코레이터를 사용하여 접근 할 수 있습니다.

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

비동기

데이터를 추출하는 과정은 대부분 비동기적 입니다. Nest도 당연히 async 함수를 통해 이를 지원합니다.

모든 비동기 함수는 Promise를 반환해야 합니다. 즉, Nest가 자체적으로 해결할 수있 는 지연된 값을 반환 할 수 있습니다.

// cats.controller.ts

@Get()
async findAll(): Promise<any[]> {
  return [];
}

요청 페이로드

이전 예제의 POST 경로 핸들러(create())는 클라이언트로부터 어떠한 값을 받지 않았습니다. 이제 클라이언트로부터 값을 받아보죠. 함수 시그니쳐@Body() 데코레이터를 추가해보겠습니다.

그 전에 먼저 DTO(Data Transfer Object) 스키마를 정해야 합니다. DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의하는 객체입니다. 타입스크립트 인터페이스를 사용하거나 단순한 클래스를 사용해서 DTO 스키마를 결정할 수 있습니다. Nest처럼 Express에 기반을 두고 그 위에 타입스크립트로 추상화한 여러 프레임워크가 있는데, 대부분은 인터페이스 대신 클래스를 사용합니다. 클래스는 Javascript ES6 표준의 일부이기 때문에 타입스크립트에서 자바스크립트로 컴파일(혹은 트랜스파일)해도 그 원형이 남아있는 반면, 타입스크립트의 인터페이스는 컴파일하면 그 원형이 사라집니다. 따라서 Nest는 런타임 중에 이를 참조할 수가 없습니다. 런타임에 변수의 메타 타입에 접근할 수 있어야 Pipe와 같은 기능이 제대로 작동할 수 있기 때문에 이는 매우 중요한 문제입니다.

그럼 CreateCatDto 클래스를 만들어 보겠습니다.

// create-cat.dto.ts

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

세 가지 기본 속성만 있습니다. 이제 CatsController 안에 새로 만든 DTO를 사용해보죠.

// cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}

오류 처리

여기에 오류 처리 (예: 예외 작업)에 대한 별도의 장이 있습니다. 이 부분은 아직 작업하지 않아서 추후 링크를 달도록 하겠습니다.

전체 리소스 샘플

아래 예제는 컨트롤러에서 CRUD(Create, Read, Update, Delete)를 다루는 방법을 제시합니다.

// cats.controller.ts

import {
  Controller,
  Get,
  Query,
  Post,
  Body,
  Put,
  Param,
  Delete,
} from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

하지만 컨트롤러가 정의가 되어도 Nest는 CatsController의 존재를 알지 못합니다. 이 클래스의 존재를 Nest에 알려야 Nest에서 CatsController의 인스턴스를 만들어서 클라이언트의 요청을 처리할 수 있습니다.

컨트롤러는 항상 모듈에 속하므로 컨트롤러가 있다면 @Module() 데코레이터 안에 controllers 배열에 컨트롤러를 포함해야 합니다. 아직 예제에는 루트인 AppModule 이외에 다른 모듈을 생성하지 않았기 때문에. AppModuleCatsController를 등록하여 사용합니다.

// app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

@Module() 데코레이터를 사용하여 모듈 클래스에 메타 데이터를 첨부했으며 Nest는 이제 어떤 컨트롤러를 마운트해야하는지 쉽게 반영 할 수 있습니다.

라이브러리별 접근 방식

지금까지 응답을 조작하는 Nest 표준 방식에 대해 논의했습니다. 응답을 조작하는 두 번째 방법은 이전에도 설명했다시피 라이브러리별 응답 객체를 사용하는 것 입니다. 특정 응답 객체를 삽입하려면 @Res() 데코레이터를 사용해야 합니다. 차이점을 보여주기 위해 CatsController를 아래와 같이 다시 작성해 보겠습니다.

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
    res.status(HttpStatus.OK).json([]);
  }
}

이 방식은 그래도 잘 작동하긴 하지만, 응답 객체(헤더 조작, 라이브러리 별 기능 등)에 대한 모든 제어권을 다루기 때문에 더 많은 유연성을 허용합니다. 권한이 크면 책임도 큰 법이기 때문에 주의해서 사용해야 합니다. 일반적으로 이 방식은 훨씬 덜 명확하고 몇 가지 단점이 있습니다. 가장 큰 단점은 코드가 플랫폼에 종속된다는 점입니다. Express 라면 Express, Fastify라면 Fastify 전용 코드를 사용하기 때문입니다. 나중에 Express에서 Fastify 혹은 그 반대로 하위 계층(웹 서버)을 변경한다면 코드를 다 뜯어 고쳐야 합니다. 또한 두 번째는 테스트하기가 어렵습니다. 응답 객체를 Mocking해야 하기 떼문입니다.

또한 위의 예제에서 인터셉터 및 @HttpCode()/ @Header()데코레이터와 같은 Nest 표준 응답 처리에 의존하는 Nest 기능과의 호환성이 사라집니다. 이 문제를 해결하려면 다음과 같이 passthrough 옵션을 true로 설정하시기 바랍니다.

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

이제 Nest 기본 응답 개체와 상호 작용할 수 있지만 (예: 특정 조건에 따라 쿠키 또는 헤더 설정) 나머지는 프레임워크에 맡깁니다.

프로바이더

프로바이더는 Nest의 기본 개념입니다. 많은 기본 Nest 클래스는 서비스(Service), 레파지토리, 팩토리, 헬퍼 등등의 프로바이더로 취급될 수 있습니다. 프로바이더의 주요 아이디어는 의존성을 주입할 수 있다는 점입니다. 이 뜻은 객체가 서로 다양한 관계를 만들 수 있다는 것을 의미합니다. 그리고 객체의 인스턴스를 연결해주는 기능은 Nest 런타입 시스템에 위임될 수 있습니다.

여기까지 공식 문서 번역입니다. 멘붕에 빠지신 분들이 계실텐데요, 무슨 소리인지 모르겠죠? Nest의 프로바이더를 쉽게 읽으려면 아래 3가지 선행 지식이 필요합니다.

  • 계층형 구조(Layered Architecture)
  • 제어 역전(IoC, Inversion of Control)
  • 의존성 주입(Dependency Injection)

계층형 구조(Layered Architecture)

Nest 공식 문서를 읽다가 뜬금 없이 서비스라는 개념이 나와서 멘붕에 빠진 초심자분들이 계실겁니다. 게다가 공식문서는 "너도 이미 서비스 알지?(찡긋)" 를 바탕으로 깔고 있어서 서비스가 무엇인지에 대해 일절 설명이 없기 때문에 더더욱 그러하죠.

서비스에 대해서 이해하려면 먼저 계층형 구조(Layered Architecture) 혹은 다층 구조(n-tier Architecture)라는 아키텍쳐 디자인을 이해해야 합니다.

제가 안드로이드 개발을 처음 접했을 때의 삽질을 예로 들겠습니다. 안드로이드는 화면 하나를 구성하기 위해선 Activity를 상속받아서 사용합니다. 이 클래스에서는 UI를 조작할 수 있는데 어떤 기능에 대한 UI와 관련된 로직을 넣었죠. 아! SQLite 도 다뤄야 했습니다. 이 앱은 하나의 화면만 가진 앱이었기에 하나의 클래스에 UI, 비즈니스로직, 저장소가 다 들어가 있었습니다.

어쨌든 앱은 정상적으로 동작했습니다. 연습삼아 만들던 앱이라서 그 후로 그냥 잊혀졌습니다. 이런 프로그램의 코드는 조금만 커져도 유지보수 난이도가 곱절로 뜁니다. 이 클래스에 UI관련 코드, 저장소 관련 코드 등이 섞여있어서 이 클래스가 무슨 목적으로 존재하는가? 에 대해서 추론하기가 힘들기 때문입니다. 이는 소프트웨어 개발 시 응집도를 높이고 결합도를 낮추라는 소프트웨어 설계법칙에 정면으로 위반합니다. 게다가 이 코드는 재활용을 하기 힘들며 특히 자동화 테스트를 하기가 힘들어집니다. 결국 더 크고 복잡한 프로그램을 만들기란 불가능해지죠.

이런 문제 해결을 위해 소프트웨어 업계에서는 계층형 구조라는 기법을 사용합니다. 복잡해 보이는 작업도 그 작업을 나누고 각 작업마다 역량을 집중하면 쉽게 해결할 수 있지요(이를 관심사 분리라고 합니다.). 한 사람이 풀스택 개발자는 이름 아래 큰 웹서비스 전체를 완벽하게 만들기란 힘들지만, 프론트엔드와 백엔드를 나눠서 작업하면 쉽게 작업할 수 있는 것 처럼 말이죠. 몇 개의 계층으로 구분하는냐에 따라서 다르지만 보통 3계층 구조를 많이 사용합니다. 영어로는 3-Tier Architecture 라고 하지요. 3계층은 아래와 같이 나눌 수 있습니다.

  • Presentation Tier: 사용자 인터페이스 혹은 외부와의 통신을 담당합니다.
  • Application Tier: Logic Tier라고 하기도 하고 Middle Tier라고 하기도 합니다. 주로 비즈니스 로직을 여기서 구현을 하며, Presentation TierData Tier사이를 연결해줍니다.
  • Data Tier: 데이터베이스에 데이터를 읽고 쓰는 역할을 담당합니다.

Nest에서의 Presentation Tier는 외부의 입력을 받아들이는 컨트롤러이며 서비스는 Application Tier에 해당합니다. 서비스에는 주로 비즈니스 로직이 들어갑니다. Nest는 이렇게 컨트롤러와 그 하위 계층을 프로바이더라는 이름으로 구분하며, 이는 응집도는 높이고 결합도는 낮추는 소프트웨어 설계입니다.

제어 역전(IoC, Inversion of Control)

제어 역전을 한 마디로 표현한다면 나 대신 프레임워크가 제어한다 입니다. 제어 역전을 설명하기 위해서는 의존성이라는 개념을 알아야하는데요.

타입스크립트를 비롯한 많은 언어에서는 클래스를 사용하려면 new 같은 키워드로 인스턴스화를 시켜야 합니다. 모름지기 사람이라면 붕어빵(인스턴스화시킨 클래스) 붕어빵틀(클래스)을 먹지는 않는 것과 같습니다.

const sword = new Sword();

Warrior클래스에서 Sword클래스를 인스턴스화 했습니다. 여기까지는 별 문제 없습니다. 필요한 클래스를 생성해서 사용하는게 무슨 문제가 있겠습니까. 다만 기획팀에서 다음 업데이트때 이제 전사는 칼 뿐만 아니라 몽둥이도 사용할 수 있다고 지령이 내려옵니다. 이미 코드 백 만군데나 Sword()를 박아뒀는데 이럴 어찌하면 좋습니까.

좋은 객체지향 설계는 구체적인 개념에 의존하지 말고 추상적 개념에 의존해야 합니다. 위 코드에서 new를 사용하면 SwordSword를 생성하는 Warrior 사이에 의존성이 생깁니다. 정확히는 WarriorSword에 의존하게 되지요. 이렇게 직접적이고 구체적으로 클래스를 인스턴스화하는 행동은 바람직하지 않습니다.

이럴때 인터페이스가 혜성처럼 나타납니다. 프로그래밍에서 인터페이스는 규약입니다. 인터페이스를 구현하려면 인터페이스가 원하는 규약을 따라야 합니다. 반대 급부로 프로그래머는 내가 호출하는 클래스가 무엇인지 정확하게 알 필요가 없습니다. 다만 특정 기능이 동작 가능하다는 사실만 알고 개발하면 됩니다. 이제 구조를 좀 바꿔보죠. 하는 김에 전사 말고 궁사나 마법사 확장도 염두에 두고 지원하도록 해봅시다.

interface Weaponable {
  swing(): void;
}

interface Playable {
  attack(): void;
}

class Warrior implements Playable {
  private Weaponable weapon;

  constructor Warrior(private readonly Weaponable _weapon) {
    weapon = _weapon;
  }

  public void attack() {
    weapon.swing();
  }
}

class Mongdungee implements Weaponable {
  public void swing() {
    console.log('Mongdungee Swing!');
  }
}

몽둥이 쥔 전사 클래스를 인스턴스화 시켜보겠습니다.

Warrior warrior = new Warrior(new Mongdungee());

참 쉽죠? 하지만 클래스 계층 구조가 복잡한 프로그램에서 직접 저 몽둥이를 넘겨야 한다거나, 여러 전사에게 같은 몽둥이를 넘기거나 한다는 상황에서는 그닥 좋지 않습니다. 이 경우 제어 역전을 사용합니다. Nest는 제어 역전을 추상화해서 그 동작이 잘 보이지 않기 때문에 typedi를 사용해서 이를 구현해보겠습니다.

import "reflect-metadata";
import { Container, Service } from "typedi";

// Weaponable, Playble 은 위와 같음

@Service()
class Mongdungee implements Weaponable {
  public void swing() {
    console.log('Mongdungee Swing!');
  }
}

@Service()
class Warrior implements Playable {
  // 아래 코드 중요!
  constructor(private readonly weapon: Weaponable) {}

  public void attack() {
    this.weapon.swing();
  }
}


const playerInstance = Container.get<Warrior>(Warrior);
playerInstance.attack();  // "Mongdungee Swing!"

코드 어디에서도 new가 없습니다. 하지만 잘 동작함을 확인할 수 있습니다. 이는 typediContainer라는 친구가 알아서 클래스의 인스턴스를 생성했기 때문입니다. 이처럼 제어권을 내가 아닌 프레임워크에게 넘기는 것이 제어 역전입니다.

여기서 라이브러리와 프레임워크의 결정적인 차이가 발생합니다. 라이브러리는 내가 짠 코드에서 필요할 때 라이브러리를 실행시킵니다. 즉 라이브러리는 제 코드의 소비자가 될 수 없습니다. 반면 프레임워크는 제 코드의 소비자가 될 수 있습니다. 프레임워크는 내가 짠 코드가 필요할 때 알아서 실행시키게 되지요.

의존성 주입(DI, Dependency Injection)

제어 역전(이하 IoC)은 나 대신 프레임워크가 제어한다라면 의존성 주입(이하 DI)은 프레임워크가 주체가 되어 네가 필요한 클래스 등을 너 대신 내가 관리해준다는 개념이라고 생각하시면 됩니다.

앗? DI는 이미 제어 역전 설명할때 나왔죠. Warrior 클래스의 생성자에서 new없이 선언만 했는데도 마치 인스턴스처럼 사용할 수 있었던 코드 말이죠! 이쯤되면 많은 분들이 제어 역전(IoC)과 의존성 주입(DI)이 헷갈리곤 합니다. 멀리서 보면 DI보다 IoC가 더 크고 추상적인 개념입니다. IoC는 추상적이기 때문에 이를 구현한게 바로 DI이며 이는 제어 역전의 구현체 중 하나입니다. 그래서 DI를 통해 IoC를 구현했다는 말이 나옵니다. Nest는 DI를 통해 IoC를 구현한 프레임워크입니다.

자, 그럼 다시 공식 문서를 읽어볼까요?

프로바이더는 Nest의 기본 개념입니다. 많은 기본 Nest 클래스는 서비스(Service), 레파지토리, 팩토리, 헬퍼 등등의 프로바이더로 취급될 수 있습니다. 프로바이더의 주요 아이디어는 의존성을 주입할 수 있다는 점입니다. 이 뜻은 객체가 서로 다양한 관계를 만들 수 있다는 것을 의미합니다. 그리고 객체의 인스턴스를 연결해주는 기능은 Nest 런타입 시스템에 위임될 수 있습니다.

프로바이더

약간 이해될듯 말듯 하시죠? 한 마디로 하자면 WarriorMongdungee가 바로 프로바이더입니다. 어떤 컴포넌트가 필요하며 의존성을 주입당하는 객체를 프로바이더라고 생각하시기 바랍니다. 그리고 Nest 프레임워크 내부에서 알아서 컨테이너를 만들어서 관리해준다는 말입니다. 이해 가시나요? 그럼 계속 진행하겠습니다 :)

이전 챕터에서 저희는 간단한 CatsController를 만들었습니다. 컨트롤러는 HTTP 요청을 처리하고 보다 복잡한 일을 프로바이더에 위임합니다. 프로바이더는 모듈에서 프로바이더로 선언된 평범한 자바스크립트 클래스입니다. 모듈은 또 추후 설명하지만 그냥 이런게 있다고만 알아두시면 됩니다.

서비스

간단한 캣츠 서비스부터 만들어 봅시다. 이 서비스는 데이터 저장 및 검색을 담당하며, CatsController가 사용하도록 설계되었으므로 프로바이더로 정의하기 좋습니다.

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

CLI로 서비스를 생성하려면 쉘에서 $ nest g service cats 명령어를 실행하시면 됩니다.

CatsService는 하나의 속성과 두 개의 메소드를 가진 기본 클래스입니다. 유일한 새로운 기능은 @Injectable() 데코레이터를 사용한다는 것 뿐입니다. @Injectable() 데코레이터는 메타 데이터를 첨부하여 CatsService가 Nest IoC 컨테이너에서 관리할 수 있는 클래스임을 선언합니다. 이 예제에서는 Cat 인터페이스도 사용하는데, 아마 다음과 같은 코드일겁니다.

// interfaces/cat.interface.ts
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

고양이 정보를 받아오는 서비스가 생겼으니 이를 CatsController 안에서 사용해보도록 하지요.

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsService는 컨트롤러 클래스 생성자를 통해 주입됩니다. 생성자의 파라미터에 주목해 주세요. 보통 Java에서 객체의 생성자는 아래와 같을겁니다.

public class User {
  private String name;
  constructor(String name) {
    this.name = name;
  }
}

타입스크립트는 자바보다는 훨씬 간단하게 생성자를 생성할 수 있습니다. 아래 코드는 위의 코드와 동일한 동작을 합니다.

class User {
  constructor(private name: string) {}
}

의존성을 주입하기 위한 방법은 크게 3가지가 있습니다.

  • 생성자를 이용한 의존성 주입(Constructor Injection)
  • 수정자를 이용한 의존성 주입(Setter Injection)
  • 필드를 이용한 의존성 주입(Field Injection)

Nest에서는 주로 생성자를 이용한 의존성 주입을 권장합니다. 필드를 이용한 의존성 주입은 한 두 스크롤 아래 속성 기반 주입(Property-based injection)이라는 항목으로 소개합니다.

범위(Scopes)

프로바이더는 일반적으로 Nest 프로그램의 수명 주기와 동기화 된 수명(범위)을 갖습니다. Nest 프로그램이 부트 스트랩 될 때 모든 종속성을 해결해야 하기 때문에 모든 프로바이더가 인스턴스화 됩니다. 마찬가지로 Nest 프로그램이 종료되면 각 프로바이더가 메모리에서 삭제됩니다. 그러나 프로바이더의 수명을 요청 단위로 제한하는 방법도 있습니다. 다만 성능에 문제가 될 수 있기 때문에 특수한 상황이 아니라면 기본 설정된 수명 주기를 사용하도록 하시기 바랍니다.

선택적 프로바이더(Optional providers)

때때로, 반드시 해결될 필요가 없는 종속성이 있을 수 있습니다. 예를 들어 클래스는 configuration 객체에 의존할 수 있지만 해당 인스턴스가 없는 경우 기본값을 사용하는 경우입니다. 이러한 경우 에러가 발생하지 않으므로 종속성이 선택사항이 됩니다.

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

프로바이더가 선택적임을 나타내려면 constructor 서명에 @Optional() 데코레이터를 사용하시길 바랍니다.

속성 기반 주입(Property-based injection)

앞서 설명한 의존성 주입의 3가지 방법 중 3번째 방법인 "필드를 이용한 의존성 주입"입니다. 지금까지 예제는 의존성이 생성자 방법을 통해 주입되기 때문에 생성자를 이용한 의존성 주입이라고 불린다. 매우 구체적인 경우 속성 기반 주입이 유용할 수 있습니다. 예를 들어 최상위 클래스가 하나 또는 여러 프로바이더에 종속되어 있는 경우 생성자에서 하위 클래스의 super()를 호출하여 해당 클래스를 끝까지 전달하는 것은 매우 지루할 수 있습니다. 이 문제를 방지하려면 속성 수준에서 @Inject() 데코레이터를 사용할 수 있습니다.

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

만약 여러분의 클래스가 다른 프로바이더를 확장(extend)하지 않는 이상 반드시 생성자를 이용한 의존성 주입을 사용하기 바랍니다.

프로바이더 등록

이제 우리는 프로바이더(CatsService)를 정의했고, 그 서비스의 소비자(CatsController)를 가지고 있으므로, 주입을 수행할 수 있도록 Nest에 서비스를 등록해야 합니다. 모듈 파일 (app.module.ts)을 편집하고 서비스를 @Module() 데코레이터의 providers 배열에 추가하면 됩니다.

Nest는 이제 CatsController 클래스의 의존성을 해결할 수 있습니다. 디렉토리 구조는 다음과 같습니다.

  • src
    • cats
      • dto
        • create-cat.dto.ts
      • interfaces
        • cat.interface.ts
      • cats.service.ts
      • cats.controller.ts
    • app.module.ts
    • main.ts

모듈

모듈은 @Module() 데코레이터가 달린 클래스 입니다. @Module() 데코레이터는 Nest가 애플리케이션 구조를 만들때 사용할 수 있는 메타데이터를 제공해주는 역할을 하지요.

Nest Module Picture1

각 응용 프로그램에는 적어도 하나 이상의 루트 모듈이 있습니다. 루트 모듈은 마치 Javapublic static void main(String[] args) 메서드처럼 Nest가 애플리케이션 그래프를 구성하기 위해 사용하는 시작점입니다. 여기서 말하는 애플리케이션 그래프는 Nest가 모듈과 프로바이더 간의 관계 및 종속성을 연결하기 위해 사용하는 내부 데이터 구조입니다. 매우 작은 응용 프로그램에는 루트 모듈 단 하나만 사용할 수 있습니다. 하지만 이는 그저 연습용이지 일반적인 경우는 아닙니다. 어플리케이션이 커지면 컴포넌트를 분리해야하고, 컴포넌트를 구성하는 효과적인 방법으로 여러 개의 모듈을 사용하는 걸 피할 수 없습니다. 따라서 대부분의 애플리케이션에서는 각각 밀접하게 관련된 기능을 묶어서 캡슐화하는 모듈을 여러개 사용합니다.

@Module() 데코레이터는 아래 속성을 가지는 객체가 필요합니다. 이 객체는 모듈을 구성하는데 필요한 정보를 가지고 있습니다.

  • providers(프로바이더): Nest 인젝터(Injector: 의존성을 주입하는 Nest 내부 모듈)가 인스턴스화시키고 적어도 이 모듈 안에서 공유하는 프로바이더.
  • controllers(컨트롤러): 이 모듈안에서 정의된, 인스턴스화 되어야하는 컨트롤러의 집합
  • imports: 해당 모듈에서 필요한 모듈의 집합. 여기에 들어가는 모듈은 프로바이더를 노출하는 모듈입니다.
  • exports: 해당 모듈에서 제공하는 프로바이더의 부분집합이며, 이 모듈을 가져오는 다른 모듈에서 사용할 수 있도록 노출할 프로바이더

노출과 내보내기
둘 다 영어 단어로 export 입니다. 직역하면 내보낸다지만 이 단어보다는 노출이라는 단어가 적절하다고 여겨서 노출이라고 했습니다. 내보낸다고 실제로 나가지는 않으니까요.

모듈은 기본적으로 프로바이더를 캡슐화합니다. 즉, 현재 모듈에 직접 속하지 않거나 가져온 모듈에서 노출(export)하지 않는 프로바이더를 주입할 수 없습니다. 따라서 모듈에서 노출한 프로바이더를 모듈의 공용 인터페이스 또는 API로 간주할 수 있습니다.

기능 모듈(Feature modules)

CatsControllerCatsService는 둘 다 고양이를 다루니 같은 도메인에 속합니다. 밀접하게 관련되어 있으므로 기능 모듈로 이동하는 것이 좋습니다. 기능 모듈은 특정 기능과 관련된 코드를 구성하여 코드를 체계적으로 유지하고 명확한 경계를 설정합니다. 이는 특히 애플리케이션이나 팀의 규모가 커짐에 따라 커지는 복잡성을 관리하는데 도움을 주고 SOLID 원칙으로 개발하는데 도움이 됩니다.

이를 증명하기 위해 CatsModule을 만들어보겠습니다.

// cats/cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

힌트
CLI를 사용하여 모듈을 생성하려면 $ nest g module cats 명령을 실행하세요.

위에서 우리 cats.module.ts파일의 CatsModule을 정의했습니다. 그리고 이 모듈과 관련된 모든 것을 cats 디렉토리로 옮겼습니다. 마지막으로 해야 할 일은 이 모듈을 루트 모듈(app.module.ts파일에 정의된 AppModule)로 가져오는 것입니다.

// app.module.ts

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

여태까지 한 작업의 디렉토리 구조는 다음과 같습니다.

  • src
    • cat
      • dto
        • create-cat.dto.ts
      • interface
        • cat.interface.ts
      • cats.controller.ts
      • cats.module.ts
      • cats.service.ts
    • app.module.ts
    • main.ts

공유 모듈(Shared modules)

Nest에서 모듈은 기본적으로 싱글톤(singleton)입니다. 이는 Nest 고유의 특성이 아니라 Node의 특성이죠. 이런 특성 때문에 Nest에서는 여러 모듈간에 쉽게 공급자의 동일한 인스턴스를 공유 할 수 있습니다.

이미지2

모든 모듈은 자동으로 공유 모듈 입니다. 즉 공유가 가능한 모듈이라는 뜻이며, 일단 생성되면 모든 모듈에서 재사용 할 수 있습니다. 다른 여러 모듈간에 CatsService의 인스턴스를 공유하는 상황을 가정해 보겠습니다. 그렇게 하려면 먼저 아래와 같이 모듈의 exports 배열에 CatsService 프로바이더를 추가하여 프로바이더를 노출해야 합니다.

//cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

이제 CatsModule을 가져오는(import)하는 모듈에서는 CatsService에 접근할 수 있으며, 이 모듈을 가져오는 다른 모듈과 동일한 인스턴스를 공유합니다.

모듈 다시 내보내기

위에서 볼 수 있듯이 모듈은 모듈 내부의 프로바이더를 노출할 수 있습니다. 또한 가져온 모듈을 다시 내보낼 수 있습니다. 아래 예를 볼까요? CommonModule을 가져와서 이를 바로 노출합니다. 그럼 CoreModule 모듈을 가져오는 다른 모듈에서는 CommonModule을 사용할 수 있게되죠.

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

의존성 주입

모듈 클래스도 프로바이더를 주입할 수 있습니다. 예를 들어 설정관련된 목적으로요.

// cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}

그러나 모듈 클래스 자체는 순환 종속성으로 인해 프로바이더로 주입할 수 없습니다.

전역 모듈(Global modules)

모든 곳에서 동일한 모듈을 가져오는 일은 지루한 일이죠. 별도의 절차 없이 어플리케이션 어디서나 사용할 수 있게 하면 좋겠습니다. 이럴 때 전역적으로(즉, 앱 전체적으로) 적용할 수 있는 수단이 필요합니다. Nest가 Angular로부터 영감을 받아서 만들어졌다는 사실은 유명합니다. 하지만 Angular와는 달리 Nest는 전역적으로 프로바이더를 등록할 수 없습니다. Nest에서는 프로바이더는 모듈을 벗어날 수가 없기 때문입니다. 해당 프로바이더를 다른 곳에서 사용하려면 해당 프로바이더가 속한 모듈을 먼저 가져 오지 않으면 안됩니다.

어디서나 사용할 수 있어야 하는 프로바이더(예: 헬퍼, 데이터베이스 연결 등)를 제공하려면 @Global() 데코레이터를 사용하여 해당 모듈을 전역 모듈으로 만드시기 바랍니다.

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

@Global() 데코레이터는 모듈을 전역적으로 사용할 수 있도록 만듭니다. 전역 모듈은 일반적으로 루트 또는 코어 모듈에 의해 단 한 번만 등록되어야 합니다. 위의 예에서 CatsService 프로바이더는 어디서나 사용할 수 있으며, CatsService 서비스를 주입하려는 모듈은 CatsModule을 모듈의 import 배열에 추가할 필요가 없습니다.

힌트
모든 모듈을 전역으로 만드는 것은 좋은 디자인이 아닙니다. 어떤 기능을 완성하기 위한 보일러플레이트 코드가 많다면 이를 줄이기 위해 전역 모듈을 사용하면 좋습니다. 모듈의 imports 배열에 넣는게 일반적으로 선호하는 방법입니다.

동적 모듈

Nest 모듈 시스템에는 동적 모듈 이라 불리는 강력한 기능이 포함되어 있습니다. 이 기능을 사용하면 프로바이더를 동적으로 등록하고 구성할 수 있는 커스터마이징 모듈을 쉽게 만들 수 있습니다. 여기서는 간략히 언급하고 나중에 별도의 장으로 자세히 설명하도록 하겠습니다. (사실 좀 어렵습니다만, 여기서 자세히 적자면 흐름이 좀 깨집니다)

다음은 데이터베이스 기능을 다루는 DatabaseModule 동적 모듈을 예로 정의했습니다.

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

힌트
forRoot() 메서드는 Promise를 통해 동적 모듈을 동기식 또는 비동기식으로 반환할 수 있습니다.

이 모듈은 기본적으로 Connection 프로바이더를 정의하지만(@Module() 데코레이터 메타데이터), forRoot() 메서드에 전달된 entitiesoptions 객체에 따라 레파지토리와 같은 여러 프로바이더를 노출합니다. 동적 모듈이 반환하는 속성은 @Module() 데코레이터에 정의된 모듈의 기본적인 메타데이터를 재정의(override)하지 않고 확장합니다. 이렇게하면 정적으로 선언된 연결 제공자와 동적으로 생성된 저장소 제공자가 모두 모듈에서 내보내집니다.

전역 범위에 동적 모듈을 등록하려면 global 속성을 true로 설정합니다.

{
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
}

경고 위에서 언급했듯이 모든 것을 글로벌하게 만드는 것은 좋은 디자인 결정이 아닙니다 .

DatabaseModule은 아래와 같은 방식으로 가져오고 설정할 수 있습니다.

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

다시 동적 모듈을 다시 내보내려면 exports 배열에서 forRoot() 메서드 호출을 생략할 수 있습니다 .

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule],
})
export class AppModule {}

동적 모듈에 대한 맛보기 설명이었는데, 사실 이 부분이 조금 어렵습니다. 나중에 기회될 때 동적모듈에 대해서 더 자세히 적도록 하겠습니다.

미들웨어

미들웨어는 클라이언트로부터 들어온 요청을 각 컨트롤러의 요청 핸들러가 처리하기 이전에 코드를 실행할 수 있는 기능입니다. 미들웨어 함수는 애플리케이션의 요청-응답 주기에서 요청(request)응답(response) 객체에 접근할 수 있으며 next()라는 미들웨어 함수를 사용할 수 있습니다. 다음 미들웨어 함수는 일반적으로 next라는 변수로 표시됩니다.

middleware

Nest 미들웨어는 기본적으로 Express의 미들웨어와 동일합니다. Express의 공식 문서에서 가져온 다음 설명은 미들웨어의 기능을 설명합니다.

미들웨어 기능은 다음을 수행할 수 있습니다.

  • 어떠한 코드를 살행할 수 있습니다.
  • 요청 및 응답 개체를 변경할 수 있습니다.
  • 요청-응답 주기를 종료합니다.
  • 스택의 다음 미들웨어 기능을 호출합니다.
  • 현재 미들웨어 기능이 요청-응답 주기를 종료하지 않는 경우, 다음 미들웨어 기능으로 제어를 전달하기 위해 next()를 호출해야 합니다. 그렇지 않으면 요청이 보류(hang)됩니다.

여러분은 이 미들웨어를 하나의 함수를 통해 구현하거나 @Injectable() 데코레이터가 있는 클래스로 구현할 수 있습니다. 클래스로 구현하려면 NestMiddleware 인터페이스를 구현해야 합니다. 먼저 클래스를 사용해서 간단한 미들웨어를 만들어보죠.

// logger.middleware.ts;

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}

위 미들웨어는 요청이 들어오면 콘솔에 "Request..."라는 문자열을 찍고 끝나기만 합니다.

의존성 주입

Nest 미들웨어는 의존성 주입(Dependency Injection)을 완벽하게 지원합니다. 의존성 주입이 무엇인지 모르신다면 여기을 참고하사기 바랍니다. 프로바이더 및 컨트롤러와 마찬가지로 동일한 모듈 내에서 사용할 수 있는 의존성을 주입할 수 있습니다. 항상 그렇듯이 이 작업은 생성자를 통해 수행됩니다.

미들웨어 적용

@Module() 데코레이터를 설정하는 속성에는 imports, exports, providers 등만 있지 미들웨어를 설정하기 위한 속성은 없습니다. 대신 모듈 클래스의 configure() 메서드를 사용하여 설정할 수 있습니다. 미들웨어를 포함하는 모듈은 NestModule 인터페이스를 구현해야 합니다. 이제 Logger 미들웨어를 AppModule 수준에서 설정하겠습니다.

// app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('cats');
  }
}

이 예제 이전에 단순히 "Request..."라고 콘솔에 찍던 미들웨어 생각나시죠? 이 미들웨어를 클라이언트에서 /cats 경로의 리소스를 요청할 경우 LoggerMiddleware를 적용하도록 설정했습니다. 특정 HTTP 메서드만 적용할 수도 있습니다. 예를 들어 /cats경로의 GET 메서드만 적용하고 싶다면 아래와 같이 RequestMethod 열거형을 불러와서 적용합니다.

// app.module.ts

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

Route wildcards

패턴 기반 라우팅 역시 지원합니다. 예를 들어 별표(*)가 와일드카드로 사용되며 다음과 같은 문자의 조합과 일치합니다.

forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

ab*cd 라우팅 경로는 abcd, ab_cd, abece 등과 일치합니다. 여기서 감이 좀 있으신 분들은 정규표현식(Regular Expression)이 떠오르실 겁니다. 정규표현식을 그대로 적으면 동작할까요? 그건 아닙니다. Nest에서 지원하는 패턴 기반 라우팅은 정규표현식의 부분집합, 즉 모든 정규표현식의 기능을 지원하지 않습니다. 라우팅 경로에서 문자 ?, +, *()를 사용할 수 있으며 하이픈(-) 및 점(.)은 문자 그대로 문자열 기반 경로로 해석됩니다. 만약 Express가 아닌 fastify를 사용하신다면 공식문서를 참고하여 사용에 주의하시기 바랍니다.

미들웨어 소비자

Nest가 제공하는 MiddlewareConsumer는 헬퍼 클래스입니다. 헬퍼 클래스가 뭐냐구요? 음.... 헬퍼 클래스는 좀 말 그대로 "도와주는" 클래스인데요, 어떤 특정 클래스의 작업을 도와주는 클래스를 통칭하는 말입니다. 절대 전면에 나서지 않는 도우미같은 존재죠. 어쨌든 MiddlewareConsumer는 말 그대로 미들웨어를 잘 사용하기 위해 유용한 기능을 제공해 줄 것 같은데요, 이 기능은 모두 플루언트 스타일로 체이닝으로 사용할 수 있습니다. forRoutes() 메서드에는 단일 문자열, 다중 문자열, RouteInfo 개체, 컨트롤러 클래스 및 여러 컨트롤러 클래스가 포함될 수 있습니다. 대부분의 경우 쉼표로 구분된 컨트롤러 목록을 전달하기만 하면 됩니다. 다음은 단일 컨트롤러의 예입니다.

// app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller.ts';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(CatsController);
  }
}

위의 코드는 CatsController가 처리하는 라우팅 경로에 LoggerMiddleware 미들웨어를 적용하겠다는 코드입니다.

경로 제외

개발하다보면 가끔씩 특정 경로 한 두개에 대해서 미들웨어가 적용시키고 싶지 않을 때가 있습니다. exclude() 메서드를 사용하면 특정 경로만 쉽게 제외할 수 있습니다. 이 메서드는 다음과 같이 제외할 경로를 식별하는 단일 문자열, 다중 문자열 또는 RouteInfo 객체를 사용할 수 있습니다.

consumer
  .apply(LoggerMiddleware)
  .exclude({ path: 'cats', method: RequestMethod.GET }, { path: 'cats', method: RequestMethod.POST }, 'cats/(.*)')
  .forRoutes(CatsController);

힌트
exclude()메서드는 path-to-regexp 패키지를 사용하여 와일드카드를 지원합니다.

위 예제처럼 LoggerMiddlewareexclude()에 정의된 3가지 패턴 외의 CatsController 경로에 미들웨어로 작동합니다.

함수형 미들웨어

지금까지 만진 LoggerMiddleware 클래스는 매우 간단합니다. 멤버도 없고, 메서드 및 종속성도 없습니다. 이런 간단한 미들웨어는 클래스 대신 간단한 함수로 정의할 수 있습니다. 이런 종류의 미들웨어는 함수형 미들웨어라고 불립니다. 그 차이를 설명하기 위해 로깅 미들웨어를 클래스 기반에서 함수형 미들웨어로 변환해 보겠습니다.

// logger.middleware.ts

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
}

그리고 이를 AppModule에서 사용하면 됩니다.

// app.module.ts

consumer.apply(logger).forRoutes(CatsController);

힌트
미들웨어에 종속성이 필요하지 않다면, 클래스형 미들웨어보다는 더 간단한 함수형 미들웨어 사용을 고려해보시기 바랍니다.

여러 미들웨어

위에서 언급한 바와 같이, 순차적으로 실행되는 여러 미들웨어를 바인딩하기 위해서는 apply() 메서드 안에 쉼표로 구분된 미들웨어 목록을 넣기만 하면 됩니다.

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

전역 미들웨어

미들웨어를 등록된 모든 경로에 한 번에 바인딩하려면 INestApplication 인스턴스에서 제공하는 use() 메서드를 사용할 수 있습니다.

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);