NestJS Overview - Middleware

Min Su Kwon·2021년 10월 17일
0

미들웨어는 라우트 핸들러로 요청이 전달되기 전에 호출되는 함수다. 미들웨어 함수는 요청/응답 객체, 그리고 next() 미들웨어 함수에 접근할 수 있다. next 함수는 next라는 이름의 변수로 표현된다.

네스트 미들웨어는 디폴트로 Express 미들웨어와 같은 구조를 지닌다. 아래의 Express doc 내용이 미들웨어의 기능을 잘 설명해준다.

  • 아무 코드나 실행가능
  • 요청/응답 객체를 변경가능
  • 요청-응답 사이클을 끝낼 수 있다
  • 스택에 있는 다음 미들웨어 함수를 호출할 수 있다
  • 현재의 미들웨어 함수가 요청-응답 사이클을 끝내지 않으면 반드시 next()를 호출해서 다음 미들웨어 함수에게 통제권을 넘겨야함

네스트 미들웨어는 함수 내에서 또는 @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

네스트 미들웨어는 의존성 주입 기능을 풀로 지원한다. 프로바이더, 컨트롤러와 동일하게, 같은 모듈에서 가용한 의존성들을 주입할 수 있다. 평소와 같이 생성자 함수 내에서.

Applying middleware

@Module 데코레이터에는 미들웨어를 위한 자리가 없다. 대신, 모듈 클래스의 configure() 메서드 내에서 설정해준다. 미들웨어를 포함하는 모듈들은 NestModule 인터페이스를 구현해야한다. LoggerMiddlewareAppModule 레벨에서 세팅해보자.

app.module.tsJS

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

위의 예시에서 LoggerMiddleware/cats 라우트 핸들러들에 대해서 설정해줬다. 특정 HTTP 메서드에서만 미들웨어가 동작하도록 추가적으로 제한할 수도 있다. 이를 위해서 pathmethod 인자를 forRoutes() 메서드로 넘겨준다.

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

패턴 기반의 라우트도 사용 가능하다.

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

이는 컨트롤러 쪽에서 사용하는 것과 같다.

Middleware consumer

MiddlewareConsumer는 헬퍼 클래스다. 이 클래스는 미들웨어 관리를 위한 몇가지 빌트인 메서드를 제공한다. 이 메서드들은 모두 체이닝 가능하다. 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.ts';

@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);

위의 예시에서, LoggerMiddlewareCatsController에 있는 모든 라우트 중 exlucde() 메서드에 명시된 라우트를 제외하고 모두 적용된다.

Functional middleware

LoggerMiddleware 클래스는 상당히 간단하다. 멤버가 없고, 추가 메서드도 없으며, 의존성도 없다. 클래스 대신에 함수로 선언할 수는 없을까? 이런 미들웨어는 함수형 미들웨어라고 부른다. LoggerMiddleware를 함수형으로 바꿔보자.

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

등록된 모든 라우트에 적용하고싶은 미들웨어가 있다면, use() 메서드를 INestApplication 인스턴스에서 호출한다.

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

글로벌 미들웨어에서 DI 컨테이너에 접근하는 것은 불가능하다. 필요하다면 함수형 미들웨어를 사용해야한다. 아니면, AppModule에서 forRoutes('*') 를 사용하는 방법이 있다.

느낀점

Express에서 많이 봤던 미들웨어고, 사용하는 방법도 똑같다. 그런데 아직 미들웨어를 뭔가 알차게(?) 활용해본 경험이 없다. 로거말고, 또 어떤 케이스에서 알차게 쓸 수 있을까?

profile
이제 막 커리어를 시작한 소프트웨어 엔지니어입니다. 배운 것을 정리하면서 조금 더 깊이 이해하려는 습관을 들이려고 합니다. 피드백은 언제나 환영입니다.

0개의 댓글