본 게시글은 공식 도큐먼트 - Middleware 를 정리한 글입니다. 내부적으로 Express 프레임워크를 다루는 경우에 대하여 설명하고 있습니다. (사실 상 원어 번역)
미들웨어(Middleware)는 라우트 핸들러의 호출 이전에 위치하는 함수이다. 미들웨어는 요청(Request)과 응답(Response)에 접근할 수 있고, 이러한 객체에 대하여 조작 또한 가능하다.
Nest.js 에서의 미들웨어는 기본적으로 Express 프레임워크와 동일하다. 따라서, Express 도큐먼트에서 정의하는 미들웨어와 본질적으로 동일하다고 할 수 있다.
Express 프레임워크 내 미들웨어에 대한 설명
-미들웨어 내에서 어떠한 코드라도 실행할 수 있다.
-요청(Request)과 응답(Response) 객체를 조작할 수 있다.
-요청과 응답 사이클(Request-Response cycle)의 시작이자, 끝이다.
-특정 미들웨어 다음에 위치하는 미들웨어를 호출할 수 있다.
-만일, 요청과 응답 사이클에서 특정 미들웨어가 마지막 미들웨어가 아니라면(즉, 그 다음에 위치한 미들웨어가 있고, 마지막 미들웨어가 해당 사이클을 종료시켜야 할 의무가 있을 경우),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();
}
}
Nest.js 의 미들웨어는 의존성 주입을 지원한다.
의존성 주입은 생성자를 통해 수행할 수 있다.
@Module()
데코레이터에서는 미들웨어를 적용할 수 있는 프로퍼티가 존재하지 않는다. 대신, configure()
이라는 메소드를 활용해 미들웨어를 적용할 수 있다.
만일, 미들웨어를 갖는 모듈이 있다면, 해당 모듈은 NestModule
인터페이스를 반드시 구현해야한다. 다음 예제는 LoggerMiddleware
를 AppModule
에 적용하는 예제이다.
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 메소드에만 적용되는 미들웨어를 등록하기 위한 코드이다.
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
패키지로부터json
과urlencoded
를 Nest 어플리케이션에 자동으로 등록한다. 따라서, 만일MiddlewareConsumer
를 통해 미들웨어를 커스텀하고 싶을 경우,NestFactory.create()
를 통해 어플리케이션을 생성할 때bodyParser
를false
로 설정하여 전역 미들웨어를 해제해야한다.
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
ab*cd
라우트 경로는 abcd
, ab_cd
, abecd
외 해당되는 패턴에 대하여 동작한다. ?
, +
, *
와 ()
같은 기호는 라우트 경로를 위해 사용될 수 있으며, -
와 .
같은 경우는 정규식으로서 사용되지 않고, 문자 그대로 사용된다.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';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController); # 컨트롤러 클래스를 인자로 전달
}
}
참고
apply()
메소드는 한 개의 미들웨어 또는 여러 개의 미들웨어를 인자로서 받을 수 있다. 자세한 내용은 다중 미들웨어 항목을 참고하자.
exclude()
메소드를 사용하여 손쉽게 미들웨어를 적용하기 원하지 않는 특정 라우트 경로를 배제시킬 수 있다. 해당 메소드는 문자열로 나타낸 단일 라우트 경로를 인자로 받을 수 있으며, 또한 여러 개의 라우트 경로들을 전달 받을 수 있다. 또한, RouteInfo
객체도 전달받을 수 있다.consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
참고
exclude()
메소드는path-to-regexp
패키지를 활용하여 와일드카드 라우트 경로 기능을 지원한다.
LoggerMiddleware
는 exclude()
메소드에 명시된 라우트 경로만 제외하고 CatsController
내 모든 라우트 경로에 대하여 적용될 것이다.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);
참고
구현한 미들웨어가 아무런 의존성도 갖고 있지 않을 경우, 간단히 함수형 미들웨어로 구성하는 것을 고려하자.
apply()
메소드에 명시하면 된다.consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
INestApplication
인스턴스에서 제공하는 use()
메소드를 사용하면 된다.const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
참고
전역 미들웨어에서 DI 컨테이너에 접근하는 것은 불가능하다. 대신, 클래스 미들웨어로 구성하고, 이를AppModule
또는 다른 모듈 내에서.forRoutes(*)
의 형태로 사용하면 된다.
끗.