Nestjs#4 MIddlewares

Nestjs 공식문서를 번역한 내용입니다. 일부 오역, 의미 전달이 모호한 부분이 있을 수 있습니다.

미들웨어는 라우트 핸들러 전에 호출되는 함수입니다. 미들웨어 함수는 request, response 객체에 접근할 수 있고, 전체 애플리케이션의 request-response 사이클에 내에 있는 next() 미들웨어 함수를 가지고 있습니다. next 미들웨어 함수는 공적으로 next라는 변수의 이름으로 표현 됩니다.

Nest 미들웨어는 기본값으로 express의 미들웨어와 동일합니다. 아래의 설명은 express 공식문서에서 표현하고 있는 미들웨어의 기능들입니다.

미들웨어 함수들은 아래의 기능들을 가지고 있습니다:

  • 임의의 코드를 실행할 수 있습니다.
  • reqeust, response 객체를 변경할 수 있습니다.
  • request-response 사이클을 종료 시킬 수 있습니다.
  • 스택 내에 있는 next 미들웨어 함수를 호출할 수 있습니다.
  • 만약 현재의 미들웨어 함수가 request-response 사이클을 종료시키지 않는다면, next()를 반드시 호출해서 다음 미들웨어로 흐름을 연결시켜야 합니다. 그렇지 않는다면 request는 지연(hanging)될 것입니다.

커스텀 Nest 미들웨어는 함수 또는 @Injectable() 데코레이터가 있는 클래스에서 구현할 수 있습니다. 클래스는 NestMiddleware를 상속받아서 구현해야하고, 함수를 통해서 구현할 때는 특별한 제약 사항이 없습니다. 그럼 클래스 메소드를 이용해서 간단한 미들웨어의 기능을 구현해 보겠습니다.

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();
  }
}

의존성 주입(Dependency injection)

Nest 미들웨어는 의존성 주입을 완전히 지원합니다. 프로바이더와 컨트롤러와 마찬가지로, 동일 모듈 내에서 의존성을 주입할 수 있습니다. 보통은 constructor를 통해서 주입됩니다.

미들웨어 적용(Applying middleware)

미들웨어를 @Module() 데코레이터의 파라미터를 통해 전달하지 않습니다. 그 대신 모듈 클래스의 configue() 메소드를 통해서 셋팅합니다. 미들웨어를 포함하는 모듈은 NestModule인터페이를 구현(implements)해야합니다. LoggerMiddlewareAppModule 레벨(최상위)에 설정해봅시다.

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'); // 전체 요청 메소드
  }
}

이전에 CatController내부에 구현했던 /cats 라우트 핸들러를 위한 LoogerMiddleware를 셋팅했습니다. 그리고 특정한 request 요청에 대한 제약하기 위해서 path와 요청 method가 포함되어 있는 객체를 forRoutes() 메소드에 전달합니다. 이 예시에서 원하는 요청 method를 지정하기 위해서 RequestMethod enum을 import하고 있습니다.

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 }); // 메소드 제약
  }
}

configure() 메소드는 async/await구문을 이용하여 비동기적으로 만들수 있습니다. (예시: configure() 메소드 내부에서await` 구분을 이용하여 비동기 동작의 완료를 기다릴 수 있습니다.)

라우트 와일드카드(Route wildcards)

패턴 기반의 라우트 또한 지원합니다. 예를 들어 아스트리크는 와일드카드로 사용됩니다. 와일드카드는 어떤 조합의 문자이더라도 일치(match)됩니다.

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

ab*cd 라우트 경로는 abcd, ab_cd, abecd 또한 포함합니다. 문자 ?, +, *, ()가 라우트 경로를 표현하는데 사용될 수 있고, 이것들은 정규식의 하위 집합입니다. 하이픈(-)과 닷(.)은 문자 그대로 문자열 기반 경로로 해석됩니다.

fasifypath-to-regexp 패키지의 최신 버전을 위용합니다. 와일드카드 아스트리크 *를 지원하지 않습니다. 대신 파라미터를 반드시 사용해야 합니다.(.*, :splat*).

미들웨어 컨슈머(Middleware consumer)

MiddlewareConsumer는 헬퍼 클래스 입니다. 미들웨어를 관리하기 위한 여러 가지 빌트인 메소드를 제공합니다. 메소드들은 fluent style로 쉽게 체이닝이 가능합니다. forRoutes() 메소드는 하나의 문자열, 여러 개의 문자열, RouteInfo 객체, 컨트롤러 클래스, 심지어는 여러개의 컨트롤러 클래스를 파라미터로 받을 수 있습니다. 대부분은 여러 컨트롤러를 콤마로 구분된 형태로 전달할 것입니다. 아래 예시는 하나의 컨트롤러 입니다:

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';

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

apply() 메소드는 하나의 미들웨어, 또는 여러 개의 미들웨어를 인자로 받을 수 있습니다.

라우트 제외(Excluding routes)

때로는 특정한 경로를 미들웨어에서 제외하고 싶을 때가 있습니다. 우리는 exclude() 메소드를 사용해서 쉽게 제외할 수 있습니다. 하나의 문자열, 여러 개의 문자열 또는 특정한 라우트정보가 포함되어 있는 RouteInfo 객체를 전달합니다:

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

exclude() 메소드는 path-to-regexp 와일드카드 파라미터를 지원합니다.

예시 코드와 같이 LoggerMiddlewareexclude() 메소드에 전달된 경로를 제외하고는 CatsController에 포함된 모든 라우트에 바인딩됩니다.

함수형 미들웨어(Functional middleware)

LoggerMiddleware 클래스는 사용하기에 간편했습니다. 멤버 속성이 없고 추가적인 메소드와 종속성이 없습니다. 그런데 왜 클래스 대신 함수를 사용할 수 없을까요? 사실 할 수 있습니다. 이러한 타입의 미들웨어를 함수형 미들웨어라고 부릅니다. 차이점을 설명하기 위해서 클래스 기반으로 구현한 logger middleware를 함수형으로 변경해보도록 하겠습니다:

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

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

구현한 내용을 AppModule에 적용합니다:

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

다른 모듈이나 코드에 종속성이 없는 미들웨어를 구현할 때는 더 간단한 형태인 함수형 미들웨어를 사용하는 것을 고려하세요.

멀티 미들웨어(Multiple middlware)

언급한 것과 같이, 순차적으로 실행되어야하는 여러 미들웨어를 연결하기 위해서, 콤마로 구분된 형태로 apply()메소드에 전달하면 됩니다:

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

전역 미들웨어(Global middlware)

만약 모든 라우트에 한번씩 등록하기를 원한다면, INestApplication의 인스턴스인 use()메소드를 사용할 수 있습니다.

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

전역 미들웨어 내에 있는 DI 컨테이너에 접근하는 것은 불가능 합니다. 대신에 app.use()를 사용하는 경우에는 함수형 미들웨어를 사용할 수 있습니다. 또는 클래스 미들웨어를 사용하여 AppModule(혹은 어떤 다른 모듈) 내에서 .forRoutes('*')를 사용할 수 있습니다.