미들웨어

미들웨어는 클라이언트로부터 들어온 요청을 각 컨트롤러의 요청 핸들러가 처리하기 이전에 코드를 실행할 수 있는 기능입니다. 미들웨어 함수는 애플리케이션의 요청-응답 주기에서 요청(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);