미들웨어는 라우트 핸들러로 요청이 전달되기 전에 호출되는 함수다. 미들웨어 함수는 요청/응답 객체, 그리고 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();
}
}
네스트 미들웨어는 의존성 주입 기능을 풀로 지원한다. 프로바이더, 컨트롤러와 동일하게, 같은 모듈에서 가용한 의존성들을 주입할 수 있다. 평소와 같이 생성자 함수 내에서.
@Module 데코레이터에는 미들웨어를 위한 자리가 없다. 대신, 모듈 클래스의 configure() 메서드 내에서 설정해준다. 미들웨어를 포함하는 모듈들은 NestModule 인터페이스를 구현해야한다. LoggerMiddleware를 AppModule 레벨에서 세팅해보자.
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 메서드에서만 미들웨어가 동작하도록 추가적으로 제한할 수도 있다. 이를 위해서 path와 method 인자를 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문법을 통해서 비동기로 만들어질 수 있다.
패턴 기반의 라우트도 사용 가능하다.
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
이는 컨트롤러 쪽에서 사용하는 것과 같다.
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()메서드는 하나의 미들웨어 또는 여러개의 미들웨어를 인자로 받을 수 있다.
미들웨어를 적용하되 몇몇 라우트에 대해서는 적용하고싶지 않을 수 있다. 이때는 exclude() 메서드를 사용한다. 하나 이상의 문자열, RouteInfo 객체 등 제외하고 싶은 라우트(들)을 인자로 받을 수 있다.
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
위의 예시에서, LoggerMiddleware는 CatsController에 있는 모든 라우트 중 exlucde() 메서드에 명시된 라우트를 제외하고 모두 적용된다.
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);
등록된 모든 라우트에 적용하고싶은 미들웨어가 있다면, use() 메서드를 INestApplication 인스턴스에서 호출한다.
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
글로벌 미들웨어에서 DI 컨테이너에 접근하는 것은 불가능하다. 필요하다면 함수형 미들웨어를 사용해야한다. 아니면,
AppModule에서forRoutes('*')를 사용하는 방법이 있다.
Express에서 많이 봤던 미들웨어고, 사용하는 방법도 똑같다. 그런데 아직 미들웨어를 뭔가 알차게(?) 활용해본 경험이 없다. 로거말고, 또 어떤 케이스에서 알차게 쓸 수 있을까?