5. Middleware
미들웨어는 라우트 핸들러 앞에 호출되는 함수입니다. 미들웨어 함수는 애플리케이션의 요청-응답 주기에서 요청 및 응답 객체와 next()
미들웨어 함수에 액세스할 수 있습니다. 다음 미들웨어 함수는 일반적으로 next
라는 변수로 표시됩니다.
Nest 미들웨어는 기본적으로 express 미들웨어와 동일합니다. 공식 express 문서의 다음 설명은 미들웨어의 기능을 설명합니다:
- 코드를 실행합니다.
- 요청과 응답 객체를 변경합니다.
- 요청-응답 사이클을 종료합니다.
- 스택에서 다음 미들웨어 함수를 호출합니다.
- 현재 미들웨어 함수가 요청-응답 사이클을 종료하지 않으면 다음 미들웨어 함수에 제어권을 넘기기 위해 next()를 호출해야 합니다. 그렇지 않으면 요청이 중단된 상태로 유지됩니다.
사용자 정의 Nest 미들웨어는 함수 또는 @Injectable() 데코레이터가 있는 클래스에서 구현합니다. 함수는 특별한 요구 사항이 없는 반면 클래스는 NestMiddleware 인터페이스를 구현해야 합니다. 클래스 메서드를 사용하여 간단한 미들웨어 기능을 구현해 보겠습니다.
주의:
Express
와fastify
는 미들웨어를 다르게 처리하고 다른 메서드 서명을 제공하며, 자세한 내용은 여기를 참조하세요.
// 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 미들웨어는 의존성 주입을 완벽하게 지원합니다. 프로바이더 및 컨트롤러와 마찬가지로 동일한 모듈 내에서 사용할 수 있는 종속성을 주입할 수 있습니다. 이 작업은 평소와 마찬가지로 생성자를 통해 수행됩니다.
Applying middleware
@Module() 데코레이터에는 미들웨어를 위한 자리가 없습니다. 대신 모듈 클래스의 configure() 메서드를 사용하여 설정합니다. 미들웨어를 포함하는 모듈은 NestModule 인터페이스를 구현해야 합니다. AppModule 수준에서 LoggerMiddleware를 설정해 보겠습니다.
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');
}
}
위의 예제에서는 이전에 CatsController 내부에 정의된 /cats 경로 핸들러에 대한 LoggerMiddleware를 설정했습니다. 또한 미들웨어를 구성할 때 경로 경로와 요청 메서드가 포함된 객체를 forRoutes() 메서드에 전달하여 미들웨어를 특정 요청 메서드로 제한할 수도 있습니다. 아래 예제에서는 원하는 요청 메서드 유형을 참조하기 위해 RequestMethod 열거형을 가져온 것을 볼 수 있습니다.
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을 사용하여 비동기화할 수 있습니다(예: configure() 메서드 본문 내에서 비동기 연산이 완료될 때까지 기다릴 수 있음).
경고:
익스프레스 어댑터를 사용할 때 NestJS 앱은 기본적으로 패키지 본문 파서에서json
및urlencoded
를 등록합니다. 즉, MiddlewareConsumer를 통해 해당 미들웨어를 사용자 정의하려면 NestFactory.create()로 애플리케이션을 생성할 때 bodyParser 플래그를 false로 설정하여 전역 미들웨어를 꺼야 합니다.
Route wildcards
NestJS 미들웨어에서도 패턴 기반 경로가 지원됩니다. 예를 들어, 명명된 와일드카드(*splat
)를 와일드카드로 사용하여 경로의 모든 문자 조합을 일치시킬 수 있습니다. 다음 예제에서는 뒤에 오는 문자 수에 관계없이 abcd/로 시작하는 모든 경로에 대해 미들웨어가 실행됩니다.
forRoutes({
path: 'abcd/*splat',
method: RequestMethod.ALL,
});
힌트:
splat은 와일드카드 매개변수의 이름일 뿐 특별한 의미는 없습니다. 예를 들어*wildcard
와 같이 원하는 이름을 지정할 수 있습니다.
'abcd/*'
경로 경로는 abcd/1, abcd/123, abcd/abc 등과 일치합니다. 하이픈( -)과 점(.)은 문자열 기반 경로에서 문자 그대로 해석됩니다. 그러나 추가 문자가 없는 abcd/는 경로와 일치하지 않습니다. 이 경우 와일드카드를 중괄호로 묶어 선택 사항으로 만들어야 합니다:
forRoutes({
path: 'abcd/{*splat}',
method: RequestMethod.ALL,
});
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() 메서드를 사용하여 쉽게 수행할 수 있습니다. exclude() 메서드는 제외할 경로를 식별하기 위해 단일 문자열, 여러 문자열 또는 RouteInfo 객체를 받아들입니다.
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/{*splat}',
)
.forRoutes(CatsController);
힌트:
exclude() 메서드는 path-to-regexp 패키지를 사용하여 와일드카드 매개변수를 지원합니다.
위의 예제에서 LoggerMiddleware는 exclude() 메서드에 전달된 세 개의 경로를 제외한 CatsController 내부에 정의된 모든 경로에 바인딩됩니다.
이 접근 방식은 특정 경로 또는 경로 패턴에 따라 미들웨어를 적용하거나 제외할 수 있는 유연성을 제공합니다.
Functional middleware
우리가 사용한 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() 메서드를 사용하면 됩니다:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(process.env.PORT ?? 3000);
힌트:
글로벌 미들웨어에서는 DI 컨테이너에 액세스할 수 없습니다. app.use()를 사용할 때 대신 함수형 미들웨어를 사용할 수 있습니다. 또는 클래스 미들웨어를 사용하고 AppModule(또는 다른 모듈) 내에서 .forRoutes('*')를 사용하여 사용할 수 있습니다.