미들웨어는 라우트 핸들러로 요청이 전달되기 전에 호출되는 함수다. 미들웨어 함수는 요청/응답 객체, 그리고 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에서 많이 봤던 미들웨어고, 사용하는 방법도 똑같다. 그런데 아직 미들웨어를 뭔가 알차게(?) 활용해본 경험이 없다. 로거말고, 또 어떤 케이스에서 알차게 쓸 수 있을까?