[Nest.js] Middleware 란 무엇인가!

정지현·2022년 11월 10일
0

본 게시글은 공식 도큐먼트 - Middleware 를 정리한 글입니다. 내부적으로 Express 프레임워크를 다루는 경우에 대하여 설명하고 있습니다. (사실 상 원어 번역)

미들웨어 (Middleware)

  • 미들웨어(Middleware)는 라우트 핸들러의 호출 이전에 위치하는 함수이다. 미들웨어는 요청(Request)과 응답(Response)에 접근할 수 있고, 이러한 객체에 대하여 조작 또한 가능하다.

  • Nest.js 에서의 미들웨어는 기본적으로 Express 프레임워크와 동일하다. 따라서, Express 도큐먼트에서 정의하는 미들웨어와 본질적으로 동일하다고 할 수 있다.

    Express 프레임워크 내 미들웨어에 대한 설명

    -미들웨어 내에서 어떠한 코드라도 실행할 수 있다.
    -요청(Request)과 응답(Response) 객체를 조작할 수 있다.
    -요청과 응답 사이클(Request-Response cycle)의 시작이자, 끝이다.
    -특정 미들웨어 다음에 위치하는 미들웨어를 호출할 수 있다.
    -만일, 요청과 응답 사이클에서 특정 미들웨어가 마지막 미들웨어가 아니라면(즉, 그 다음에 위치한 미들웨어가 있고, 마지막 미들웨어가 해당 사이클을 종료시켜야 할 의무가 있을 경우), next() 함수를 호출시켜서 그 다음 미들웨어가 계속하여 동작을 수행할 수 있도록 해야한다. 그렇지 않으면 요청이 종료되지 않고 어플리케이션 어딘가에서 걸려버리는 상태가 발생한다고 한다.

  • Nest 내에서의 미들웨어는 @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();
  }
}

의존성 주입 (Dependency Injection)

  • Nest.js 의 미들웨어는 의존성 주입을 지원한다.

  • 의존성 주입은 생성자를 통해 수행할 수 있다.

미들웨어 적용하기 (Applying Middleware)

  • @Module() 데코레이터에서는 미들웨어를 적용할 수 있는 프로퍼티가 존재하지 않는다. 대신, configure() 이라는 메소드를 활용해 미들웨어를 적용할 수 있다.

  • 만일, 미들웨어를 갖는 모듈이 있다면, 해당 모듈은 NestModule 인터페이스를 반드시 구현해야한다. 다음 예제는 LoggerMiddlewareAppModule 에 적용하는 예제이다.

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');
  }
}
  • 상기 예제에서, LoggerMiddleware 를 이전에 구현한 CatsController 컨트롤러 내에 위치한 /cats 라우트 핸들러에 부착시켰다.

  • 이러한 방식으로 미들웨어를 특정 라우트에만 국한시키고 싶을 경우, forRoutes() 메소드에 적용되기를 원하는 라우트를 명시할 수 있다. 하기 코드는 원하는 라우트 경로와 RequestMethod 를 통해 특정 HTTP 메소드에만 적용되는 미들웨어를 등록하기 위한 코드이다.

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

참고
`async/await 를 활용하여 configure() 메소드를 비동기적으로 사용할 수 있다.

주의 (뭔 말일까..)
내부적으로 Express 를 사용할 때, Nest.js 는 body-parser 패키지로부터 jsonurlencoded 를 Nest 어플리케이션에 자동으로 등록한다. 따라서, 만일 MiddlewareConsumer 를 통해 미들웨어를 커스텀하고 싶을 경우, NestFactory.create() 를 통해 어플리케이션을 생성할 때 bodyParserfalse 로 설정하여 전역 미들웨어를 해제해야한다.

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

  • 미들웨어에서 특정 라우트에 대하여 적용하고 싶을 때, 와일드카드 패턴 또한 지원한다.
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
  • ab*cd 라우트 경로는 abcd, ab_cd, abecd 외 해당되는 패턴에 대하여 동작한다. ?, +, *() 같은 기호는 라우트 경로를 위해 사용될 수 있으며, -. 같은 경우는 정규식으로서 사용되지 않고, 문자 그대로 사용된다.

미들웨어 컨슈머 (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() 메소드를 사용하여 손쉽게 미들웨어를 적용하기 원하지 않는 특정 라우트 경로를 배제시킬 수 있다. 해당 메소드는 문자열로 나타낸 단일 라우트 경로를 인자로 받을 수 있으며, 또한 여러 개의 라우트 경로들을 전달 받을 수 있다. 또한, 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 클래스는 상당히 간단한 형태의 미들웨어였다. 해당 미들웨어 내에는 추가적인 메소드나, 의존성 또한 존재하지 않았다. 이러한 미들웨어는 함수 형태로 나타낼 수 있는데, 이것을 함수형 미들웨어라고 부른다. 다음 예제는 클래스 형태로 구성되어있던 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() 메소드를 사용하면 된다.

main.ts

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

참고
전역 미들웨어에서 DI 컨테이너에 접근하는 것은 불가능하다. 대신, 클래스 미들웨어로 구성하고, 이를 AppModule 또는 다른 모듈 내에서 .forRoutes(*) 의 형태로 사용하면 된다.

끗.

profile
나를 성장시키는 좌절에 감사하고 즐기려고 노력 중

0개의 댓글