Skip to content

Nest.js Overview Middleware

Je-Gwan O edited this page Feb 15, 2022 · 1 revision

Middleware

미들웨어는 라우트 핸들러 전에 호출되는 하나의 함수입니다. 미들웨어 함수는 request 및 response 객체에 접근할 수 있으며 애플리케이션의 request-response 사이클 안에 있는 next() 미들웨어 함수에 액세스할 수 있습니다. next 미들웨어 함수는 보통 next라는 이름의 변수로 표시됩니다.

image

Nest 미들웨어는 기본적으로 express 미들웨어와 같습니다(!다만 우리가 fastify를 플랫폼으로 채택했다면 조금 다를 수 있습니다). 다음은 express 문서에서 발췌한 미들웨어의 기능입니다.

미들웨어 함수는 다음과 같은 작업을 수행할 수 있습니다.

  • 코드의 실행
  • request, response 오브젝트의 변형
  • request-response cycle을 끝내기
  • stack에 있는 다음 미들웨어 함수 실행
  • 현재 미들웨어가 req-res 사이클을 끝내지 않으면 반드시 next()를 호출하여 컨트롤을 다음 미들웨어 함수에 주어야 합니다. 그렇지 않으면 request는 hanging 상태로 머물게 됩니다.(응답을 주지 않은 채 대기)

우리는 Nest 미들웨어를 함수 또는 @injectable() 데코레이터를 포함한 클래스로 만들 수 있습니다. 클래스는 NestMiddleware 인터페이스를 implements 해야합니다. 함수는 특별한 준비물이 필요 없습니다. 그럼 클래스 메소드로 심플한 미들웨어 기능을 만들어봅시다.

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

네스트 미들웨어는 의존성 주입을 fully support 합니다. 프로바이더와 컨트롤러처럼 동일 모듈 내에서 의존성을 주입할 수 있습니다. 평소처럼 constructor를 통해 주입됩니다.

Applying middleware

@Module() 데코레이터에는 미들웨어를 위한 자리가 없습니다. 대신 Module 클래스의 configure() 메소드를 통해 장착합니다. 미들웨어를 가지고 있는 모듈은 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')
  }
}

위의 예시에서는 /cats 라우트에 대해 LoggerMiddleware를 적용했습니다. forRoutes() 메소드에 path, method등을 포함한 객체를 전달하여 좀 더 스페시픽한 request에만 지정할수도 있습니다. 아래 예시가 그렇습니다. (RequestMethod enum을 이용했습니다.)

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)

Route wildcards

패턴화된 라우트도 지ㅜ언됩니다. 예를들어 다음은 *(asterisk) 가 와일드카드로 쓰였습니다.

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

ab*cd 라우트는 abcd, ab_cd 등에 매칭됩니다. ?, +, *() 도 경로에 사용될 수 있습니다. -.은 그대로 해석됩니다.

fastify에서는 오래된 버전의 path-to-regex를 쓰기 때문에 * 와일드 카드를 지원하지 않습니다. 대신 반드시 parameter를 써야합니다. (e.g. (.*) , :splat*)

Middleware consumer

MiddlewareConsumer는 helper class 입니다. 미들웨어를 관리하기 위한 몇가지 내장 메서드를 제공합니다. 모두 fluent style(자바스크립트의 체이닝 메소드와 같은 디자인 패턴입니다. 모든 메서드의 리턴값이 자기 자신-this, 메서드가 속한 클래스-을 리턴하는 식으로 구현합니다)로 chained 될 수 있습니다. 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() 메소드 또한 하나 이상의 미들웨어를 전달 받을 수 있습니다. 👉 자세히 multiple middelwares

Excluding routes

때때로 미들웨어가 적용될 때 어떤 routes는 제외하고 싶을 수 있는데요. exclude() 메소드를 사용하면 됩니다. 이 메서드는 forRoute()와 같은 형태의 인자를 받을 수 있습니다.

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

exclude() 역시 path-to-regex 패키지를 이용해 wildcard 파라미터를 지원합니다.

위의 예시에서는 정의된 3가지 route만 제외하고 미들웨어가 적용됩니다.

Functional middleware

예시로 들은 LoggerMiddleware은 상당히 단순했습니다. 멤버도 없고 메서드도 없고, 의존성도 없었죠. 이럴 땐 단순한 함수로 정의할 수도 있습니다. 이러한 미들웨어 타입을 functional middleware라고 합니다. 이 로거 미들웨어를 펑셔널 미들웨어로 바꿔봅시다.

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

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

AppModule에서의 사용법은 아래와 같습니다.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
import { logger } 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(logger).forRoutes(CatsController)
  }
}

functional middleware는 의존성이 필요 없을 때 쓰기 좋습니다.

Multiple middleware

위에 언급했던 것처럼 여러개의 미들웨어를 순차적(sequentially)으로 실행하기 위해서는 단순히 ,로 구분하여 전달하면 됩니다.

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

Global middleware

모든 라우트에 적용될 미들웨어가 필요하다면 INestApplication인스턴스의 use() 메서드를 사용할 수 있습니다.(express처럼요)

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

글로벌 미들웨어에서 DI(의존성 주입) 컨테이너를 접근하는 것은 불가능합니다. 이럴 땐 위의 AppModule의 컨슈머를 사용하는 형식으로 쓰면됩니다.