Middleware

Middleware는 route handler가 호출되기 전에 실행되는 함수이다. NestJs에서는 middleware는 Express middleware와 동일한 기능을 가진다.
NestJS에서 미들웨어는 요청과 응답 사이에 실행되는 함수. 요청을 처리하는 도중 특정 작업을 수행할 수 있게 한다.
http 요청과 응답 사이 즉 라우터 핸들러 이전에 호출되는 함수이다.
다시 말해서 클라이언트 요청을 라우터 핸들러가 받기전에 가로채서 다른 작업을 수행할 수 있다.


다음과 같은 특징들이 있다.

  • 요청에 대한 검증, 로깅, 인증, 권한 체크, 캐싱 등의 작업을 수행할 수 있다.
  • 다수의 미들웨어 등록을 토해 다양한 작업을 순차적으로 처리할 수 있다.
  • next 함수 호출을 통해 미들웨어 체인을 연결한다. next 함수를 호출하면 다음 미들웨어가 실행되며, 호출하지 않으면 다음 미들웨어가 실행되지 않는다.
  • NestJS는 전역 미들웨어(Global Middleware)로컬 미들웨어(Local Middleware)를 지원한다. 전역 미들웨어는 모든 요청과 응답에 적용되며, 로컬 미들웨어는 특정 라우터에만 적용된다.

Middleware는 전에 route handler라고 불리던 함수이다. Middleware 함수는 request 그리고 response 객체에 접근한다. 그리고 application의 request-response cycle 에서의 next() middleware 함수에 접근한다. next middleware 함수는 보통 변수명 next로 표기된다.


next() 는 middleware function에서 중요한 역할을 하는 callback 함수이다. 이는 다음 middleware function이나 route handler로 제어를 넘기는 역할을 한다. 구체적으로는 다음과 같은 역할을 한다.


  1. 제어 흐름 유지: middleware function은 request와 response 객체에 접근할 수 있는데, next()를 호출하지 않으면 요청은 해당 middleware에서 멈추고, 이후의 middleware 함수나 route handler로 제어가 넘어가지 않는다.
  2. middleware chain 형성: 여러 개의 middleware 함수가 존재할 때, next()를 사용하여 순서대로 미들웨어를 실행할 수 있다.
  3. 오류 처리: next(err)의 형태로 오류가 발생했음을 알릴 수 있다. 이 경우 프레임워크에서 오류 처리 middleware를 찾아서 실행하게 된다.


Nest middleware은 기본적으로 express middleware와 동등하다. 다음의 설명은 공식적인 'express'의 docs에서 가져온 것으로 middleware의 capability가 담겨있다.


Middleware function은 다음의 task를 수행할 수 있다.
    1. 임의의 code들을 실행할 수 있다.
    2. request, response object들을 변경할 수 있다.
    3. request-response cycle을 끝낼 수 있다.
    4. stack에서 다음 middleware function을 호출할 수 있다.
    5. 현재 middleware function가 request-response cycle을 종료하지 못하면, 
        제어권을 다음 middleware function으로 넘기기 위해 반드시 next()를 호출해야한다.
        그렇지 않으면 request는 처리되지 않은 상태로 남게 된다.

custom Nest middleware는 함수로 구현하거나, @Injectable(). decorator가 있는 클래스로 구현할 수 있다. class의 경우 반드시 NestMiddleware interface를 구현해야 한다. 반면에 함수의 경우 다른 특별한 requirement는 없다. 이제 class method를 사용하여 간단한 middleware을 만들어보자


// 클래스 기반 미들웨어
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  // req: 요청 객체, res: 응답 객체, next: 다음 미들웨어를 호출
  // use method는 요청 정보를 출력하고, 응답 완료 후에는 응답 코드를 출력
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`Request...`);
    // next를 통해 현재 미들웨어의 처리가 완료되었음을 알리고, 다음 미들웨어로 제어를 전달한다.
    // 이를 호출하지 앟으면 요청 처리가 중단된다.
    next();
  }
}

// 함수 기반 미들웨어
import { Request, Response, NextFunction } from 'express';

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

주의사항
'Express'나 'Fastify' 는 middleware를 다르게 처리하고, 또한 다른 method signature를 제공한다. read More


method signature: 메서드의 정의에서 메서드의 이름과 매개변수 리스트의 조합을 의미한다. 이는 오버로딩과  관련이 있다.

Dependency injection

Nest의 middleware는 DI를 지원한다. provider와 controller와 같이, 같은 module안에서 inject dependencies가 가능하다. 유사하게, constructor을 통해 수행된다.

Applying middleware

@Module() decorator안에는 middleware를 위한 공간은 없다. 대신에 module class의 configure() method를 통해 설정할 수 있다.
middleware를 포함하는 Module은 반드시 NestModule interface를 구현해야 한다.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
// middleware를 설정하기 위해 configure를 호출해야한다. 그러기 위해서는
// NestModule을 implements 해야한다.
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) 
      .forRoutes('cats'); // cats 경로에 적용한다.
  }
}

위에서는 /cats route handler 를 위한 LoggerMiddleware를 설정했다. 또한 middleware를 특정 request method에 제한 할 수 있는데, 이를 위해 middleware를 구성할 때 route path와 request method가 포함된 객체를 forRoutes() method에 전달하여 middleware를 구성할 수 있다. 아래의 코드에서 우리는 request method type을 참고하기 위해 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() method는 async/await를 사용하여 asynchronous 하게 만들어졌다. configure() method는 NestJs 어플리케이션을 설정하는 데 사용되며, 이 method 안에서 비동기 작업을 수행할 수 있다. 예를 들어 db 연결 설정, 환경 변수 로드 등과 같은 작업을 configure() method 내부에서 비동기적으로 수행할 수 있다.

<주의사항>
express adapter를 사용할 때, NestJS app은 기본적으로 package body-parser를 통해 jsonurlencoded을 등록할 것이다. 그렇기에 만일 MiddlewareConsumer를 통해 middleware를 customize를 한다면, NestFactory.create() 로 application을 만들 때 global middleware를 bodyParser flag를 false로 함으로써 꺼야 한다.



Route wildcard

route base의 Pattern들 또한 지원된다. 예를 들어 wildcard로써 asterisk(*)가 사용되고 또 어떤 문자의 조합과 매칭된다.

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

'ab*cd' route path는 'abcd', 'ab_cd', 'abecd' 과 매칭될 것이다. 이 밖에 ?, +, *, () 또한 route path에 사용된다. 그리고 이들은 regular expression의 짝이다. 단순히 hyphen(-), dot(.)은 문자로 해석된다.

<주의사항!>
fastify package는 path-to-regexp package의 최신버전을 사용한다. 이는 더 이상 asterisk(*)를 지원하지 않는다. 대신,반드시 parameters를 사용해야 한다.(e.g. '(.*)', :splat*))



Middleware consumer

MiddlewareConsumer은 helper class이다. 이는 middleware를 관리하기 위한 내장 method를 제공한다. 모든 method는 fluent stylechained 된다. forRoutes() method는 single string, multiple stirngs, a RouteInfo object, a controller class, 심지어 multiple controller classes를 값으로 받을 수 있다. 보통은 comma(,)로 구분된 controller의 리스트를 전달한다.

//fluent style: 각 메서드 호출이 자신을 반환하여 다음 메서드를 바로 호출할 수 있게하는 코드 기법

consumer
  .apply(LoggerMiddleware)
  .forRoutes('cats')
  .apply(AnotherMiddleware)
  .forRoutes('dogs');

아래의 코드는 CatsController의 모든 route handler에 대해 middleware인 LoggerMiddleware를 적용한다는 의미이다.

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);// CatsController에 들어있는 route handler에 적용
  }
}

apply() method는 single middleware를 받거나, 여러 개의 argument를 받을 것이다. [middleware](https://docs.nestjs.com/middleware#multiple-middleware)



Excluding routes

때때로 middleware의 적용에서 특정한 route를 제외(exclude) 할 수 있다. exclude() method를 통해 쉽게 route를 제외할 수 있다. 이 method는 single string, multiple strings, 또는 route를 나타내는 Route Info object 를 인자로 받아 route를 제외한다.

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

<팁!>
exclude() method는 path-to-regexp package를 이용한 wildcard parameter를 지원한다.


위의 예에서, LoggerMiddleware는 CatsController 내부에 정의된 모든 route들을 둘러싼다. excpet에 적혀있는 것을 빼고.



Functional middleware

앞의 LoggerMiddleware class는 단순한 기능만 넣었다. member, method, dependency 다 없다. 그렇다면 class 대신에 간단한 function으로 작성할 수 있는데 이러한 middleware type을 functional middleware라고 한다. class 기반의 logger middleware를 functional 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);

<팁!> 만일 별다른 dependency가 없다면 functional middleware를 사용를 고려해보라.



Multiple middleware

언급한 대로, 여러 개의 middleware를 묶기위해서는 단순히 comma로 구분한 list를 apply() method에 넣어라.

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


Global middleware

만일 한 번에 구분된 모든 route에 middleware를 적용하고 싶다면, use() method를 사용하면 된다. 이는 INestApplication instance에 의해 지원된다.

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

global middleware에서 DI(Dependency Injection) 컨테이너에 접근하는 것이 불가능하다. 대신, app.use()를 사용할 때는 함수형 미들웨어를 사용할 수 있다. 또 다른 방법으로는 class 미들웨어를 사용하고, AppModule 내에서 .forRoutes('*')를 사용하여 모든 경로에 적용할 수 있다.

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Pipes  (0) 2024.06.06
Exception filters  (0) 2024.06.05
Modules  (1) 2024.06.04
Providers  (0) 2024.06.04
Controllers  (0) 2024.06.03

+ Recent posts