5. Middleware

미들웨어는 라우트 핸들러 앞에 호출되는 함수입니다. 미들웨어 함수는 애플리케이션의 요청-응답 주기에서 요청 및 응답 객체와 next() 미들웨어 함수에 액세스할 수 있습니다. 다음 미들웨어 함수는 일반적으로 next라는 변수로 표시됩니다.

nest_middleware1.png

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

사용자 정의 Nest 미들웨어는 함수 또는 @Injectable() 데코레이터가 있는 클래스에서 구현합니다. 함수는 특별한 요구 사항이 없는 반면 클래스는 NestMiddleware 인터페이스를 구현해야 합니다. 클래스 메서드를 사용하여 간단한 미들웨어 기능을 구현해 보겠습니다.

주의: Expressfastify는 미들웨어를 다르게 처리하고 다른 메서드 서명을 제공하며, 자세한 내용은 여기를 참조하세요.

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

Dependency injection

Nest 미들웨어는 의존성 주입을 완벽하게 지원합니다. 프로바이더 및 컨트롤러와 마찬가지로 동일한 모듈 내에서 사용할 수 있는 종속성을 주입할 수 있습니다. 이 작업은 평소와 마찬가지로 생성자를 통해 수행됩니다.

Applying middleware

@Module() 데코레이터에는 미들웨어를 위한 자리가 없습니다. 대신 모듈 클래스의 configure() 메서드를 사용하여 설정합니다. 미들웨어를 포함하는 모듈은 NestModule 인터페이스를 구현해야 합니다. AppModule 수준에서 LoggerMiddleware를 설정해 보겠습니다.

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

위의 예제에서는 이전에 CatsController 내부에 정의된 /cats 경로 핸들러에 대한 LoggerMiddleware를 설정했습니다. 또한 미들웨어를 구성할 때 경로 경로와 요청 메서드가 포함된 객체를 forRoutes() 메서드에 전달하여 미들웨어를 특정 요청 메서드로 제한할 수도 있습니다. 아래 예제에서는 원하는 요청 메서드 유형을 참조하기 위해 RequestMethod 열거형을 가져온 것을 볼 수 있습니다.

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() 메서드 본문 내에서 비동기 연산이 완료될 때까지 기다릴 수 있음).

경고:
익스프레스 어댑터를 사용할 때 NestJS 앱은 기본적으로 패키지 본문 파서에서 jsonurlencoded를 등록합니다. 즉, MiddlewareConsumer를 통해 해당 미들웨어를 사용자 정의하려면 NestFactory.create()로 애플리케이션을 생성할 때 bodyParser 플래그를 false로 설정하여 전역 미들웨어를 꺼야 합니다.

Route wildcards

NestJS 미들웨어에서도 패턴 기반 경로가 지원됩니다. 예를 들어, 명명된 와일드카드(*splat)를 와일드카드로 사용하여 경로의 모든 문자 조합을 일치시킬 수 있습니다. 다음 예제에서는 뒤에 오는 문자 수에 관계없이 abcd/로 시작하는 모든 경로에 대해 미들웨어가 실행됩니다.

forRoutes({
  path: 'abcd/*splat',
  method: RequestMethod.ALL,
});

힌트:
splat은 와일드카드 매개변수의 이름일 뿐 특별한 의미는 없습니다. 예를 들어 *wildcard와 같이 원하는 이름을 지정할 수 있습니다.

'abcd/*' 경로 경로는 abcd/1, abcd/123, abcd/abc 등과 일치합니다. 하이픈( -)과 점(.)은 문자열 기반 경로에서 문자 그대로 해석됩니다. 그러나 추가 문자가 없는 abcd/는 경로와 일치하지 않습니다. 이 경우 와일드카드를 중괄호로 묶어 선택 사항으로 만들어야 합니다:

forRoutes({
  path: 'abcd/{*splat}',
  method: RequestMethod.ALL,
});

Middleware consumer

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

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

힌트:
apply() 메서드는 단일 미들웨어를 받거나 여러 미들웨어를 지정하기 위해 여러 인수를 받을 수 있습니다.

Excluding routes

특정 경로를 미들웨어 적용 대상에서 제외해야 할 때가 있습니다. 이는 exclude() 메서드를 사용하여 쉽게 수행할 수 있습니다. exclude() 메서드는 제외할 경로를 식별하기 위해 단일 문자열, 여러 문자열 또는 RouteInfo 객체를 받아들입니다.

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

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

위의 예제에서 LoggerMiddleware는 exclude() 메서드에 전달된 세 개의 경로를 제외한 CatsController 내부에 정의된 모든 경로에 바인딩됩니다.

이 접근 방식은 특정 경로 또는 경로 패턴에 따라 미들웨어를 적용하거나 제외할 수 있는 유연성을 제공합니다.

Functional middleware

우리가 사용한 LoggerMiddleware 클래스는 매우 간단합니다. 멤버도 없고, 추가 메서드도 없으며, 종속성도 없습니다. 클래스 대신 간단한 함수로 정의할 수 없는 이유는 무엇일까요? 사실 가능합니다. 이러한 유형의 미들웨어를 함수형 미들웨어라고 합니다. 차이점을 설명하기 위해 로거 미들웨어를 클래스 기반에서 함수형 미들웨어로 변환해 보겠습니다:

// logger.middleware.ts
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 middleware

위에서 언급했듯이 순차적으로 실행되는 여러 미들웨어를 바인딩하려면 apply() 메서드 안에 쉼표로 구분된 목록을 제공하기만 하면 됩니다:

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

Global middleware

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

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(process.env.PORT ?? 3000);

힌트:
글로벌 미들웨어에서는 DI 컨테이너에 액세스할 수 없습니다. app.use()를 사용할 때 대신 함수형 미들웨어를 사용할 수 있습니다. 또는 클래스 미들웨어를 사용하고 AppModule(또는 다른 모듈) 내에서 .forRoutes('*')를 사용하여 사용할 수 있습니다.