NestJS 帶你飛! 시리즈 번역 11# Middleware

2023. 6. 4. 13:44개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다. 

Middleware는 무엇인가요?

Middleware는 라우터 처리 전에 실행되는 함수입니다.
요청객체와 응답객체에 접근할 수 있으며 next()를 통해 해당 요청에 대한 처리를 이어갈 수 있습니다.
ex: 다음 Middleware 실행, 실제 Resource 처리 단계 진입 등..

Express를 써봤다면 Middleware 개념이 그렇게 낯설진 않을겁니다.

사실 Nest의 Middleware와 Express의 Middleware는 같습니다.
그렇다면 Middleware는 어떤 기능을 하는지 Express 공식 사이트의 설명을 볼까요?

  • 어떤 작업이든 실행할 수 있습니다.
  • 요청 객체 또는 응답 객체를 수정할 수 있습니다.
  • 전체 요청 주기를 종료할 수 있습니다.
  • 다음 실행 단계를 호출할 수 있습니다.
  • 미들웨어에서 요청 주기를 종료하지 않았을 경우 next()를 사용해 다음 실행 할 단계를 호출해야 합니다.

Middleware 설계

Middleware는 두가지의 방법으로 설계할 수 있습니다.
일반 function이나 @Injetable 데코레이터로 NestMiddleware 인터페이스의 class를 구현하면 됩니다.

 

Functional Middleware

이런 Middleware는 정말 단순하게 생겼습니다.
일반적인 function이랑 같으니까요. 하지만 3개의 파라미터를 가지고 있는데요.

Request, ResponseNextFunction이 있습니다. 어딘가 익숙하지 않나요?

맞습니다. Express의 Middleware와 같습니다.

아래는 간단한 예제입니다.
이 함수가 끝날 때 next() 함수를 호출해줌으로써 다음 실행 스텝으로 넘김을 명시해줍니다.

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log('Hello Request!');
  next();
}

Class Middleware

이 Middleware는 CLI를 통해 생성할 수 있습니다.

$ nest generate middleware <MIDDLEWARE_NAME>
주의: <MIDDLEWARE_NAME>은 경로를 포함할 수 있습니다. ex: middlewares/logger
경로를 포함하여 생성하면 src 경로가 포함된 Middleware가 생성됩니다.

LoggerMiddlewaremiddlewares 폴더 안에 생성해보겠습니다.

$ nest generate middleware middlewares/logger

src 아래 middlewares 폴더가 생성되어 logger.middleware.tslogger.middleware.spec.ts가 생성된걸 확인할 수 있습니다.

Middleware 뼈대는 아래와 같습니다. use(req: any, res:any, next: () => void)라는 메서드가 보입니다.

이곳이 바로 로직을 처리하는 곳입니다.

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    next();
  }
}

잠깐. 왜 파라미터에 any가 붙었을까요?

이유는 기반이 될 프레임워크가 어떤것인지 모르기 때문입니다. Express라면 아래와 같은 모습일 것입니다.

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    next();
  }
}

Middleware 사용하기

Middleware의 사용 방법은 데코레이터를 사용하여 설정하는것이 아니라
Module에서 NestModule 인터페이스를 구현 후 configure() 메서드를 설계 합니다.
다시 MiddlewareConsumer 라는 Helper Class로 각각의 Middleware를 관리합니다.

아래 가장 기본적인 Middleware의 사용 방식을 소개하겠습니다.

먼저 LoggerMiddleware를 다음과 같이 수정해보겠습니다.

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Hello Request!');
    next();
  }
}

다음으로 TodoModuleTodo Controller를 생성해보겠습니다.

$ nest generate module features/todo
$ nest generate controller features/todo

todo.controller.ts를 수정해보겠습니다.

import { Controller, Get, Param } from '@nestjs/common';

@Controller('todos')
export class TodoController {
  @Get()
  getAll() {
    return [];
  }

  @Get(':id')
  get(@Param('id') id: string) {
    return { id };
  }
}

AppModuleNestModuleconfigure(consumer: MiddlewareConsumer) 메서드를 구현합니다.

apply를 통해 해당 Middleware를 적용시키고 forRoutes를 사용하여 해당 Middleware를 사용할 라우터를 설정합니다.

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [
    AppService
  ]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('/todos')
  }
}

http://127.0.0.1:3000/todos, http://127.0.0.1:3000/todos/1에 접속시 아래의 결과를 확인할 수 있습니다.

하지만 127.0.0.1:3000에 접속하면 어떠한 결과를 확인할 수도 없습니다.

Hello Request!
알림: forRoutes는 와일드카드(*) 라우팅도 지원합니다. 

여러개의 라우터를 적용하고 지정된 Http Method 사용하기

forRoutes는 다중 라우팅을 지원합니다. 라우터에 파라미터를 추가하는것으로 간단하게 사용할 수 있습니다. 

조금 특이한건 Http Method와 라우팅 경로를 지정할 수 있다는 것입니다.
pathmethod를 포함한 객체를 forRoute안에 작성해주면 됩니다. 아래는 AppModule을 수정한 예제입니다.

import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [
    AppService
  ]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(
      { path: '/todos', method: RequestMethod.POST }, // POST /todos에 유효
      { path: '/', method: RequestMethod.GET } // GET / 에 유효
    )
  }
}

웹 브라우저를 열어 http://127.0.0.1:3000/todos에 접속하면 어떠한 결과도 표시되지 않습니다.

하지만 http://127.0.0.1:3000 접속하면 터미널에 다음과 같은 결과가 출력됩니다.

Hello Request!

Controller에 적용하기

forRoutes는 해당 Controller 전체에 적용하는것도 지원합니다.
해당 Controller 아래 모든 자원에 지정된 Middleware가 작동하도록 하게 할 수 있습니다.

AppModule을 다음과 같이 수정해보겠습니다.

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './features/todo/todo.controller';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [
    AppService
  ]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes(TodoController)
  }
}

http://127.0.0.1:3000/todos와 http://127.0.0.1:3000/todos/1에 접속하면 같은 결과를 확인할 수 있습니다.

Hello Request!

특정 라우터와 지정된 Http Method 제외하기

exclude를 통해 특정 라우팅을 지정할 수 있습니다.

forRoutes와 사용방법은 비슷하지만 pathmethod의 객체를 설정해주면 됩니다.

AppModule을 수정해보겠습니다.

import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './features/todo/todo.controller';
import { TodoModule } from './features/todo/todo.module';
import { LoggerMiddleware } from './middlewares/logger.middleware';

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [
    AppService
  ]
})
export class AppModule  implements NestModule{
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).exclude(
      { path: '/todos', method: RequestMethod.GET } // GET /todos 제외
    ).forRoutes(TodoController)
  }
}

 

http://127.0.0.1:3000/todos에 접속해보면 어떠한 결과도 확인할 수 없습니다.

 

다중 Middleware 적용하기

apply는 다중 Middleware를 지원합니다. Middleware에 파라미터를 추가하는 방법으로 사용할 수 있습니다.

HelloWorldMiddleware를 하나 만들어보겠습니다.

$ nest generate middleware middlewares/hello-world

그리고 hello-world.middleware.ts를 수정해보겠습니다.

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class HelloWorldMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Hello World!');
    next();
  }
}

다음으로 AppModule를 수정해보겠습니다.

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoController } from './features/todo/todo.controller';
import { TodoModule } from './features/todo/todo.module';
import { HelloWorldMiddleware } from './middlewares/hello-world.middleware';
import { LoggerMiddleware } from './middlewares/logger.middleware';

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [
    AppService
  ]
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware, HelloWorldMiddleware).forRoutes(TodoController)
  }
}

http://127.0.0.1:3000/todos에 접속하면 터미널에서 다음과 같은 결과를 확인할 수 있습니다.

Hello Request!
Hello World!

전역 Middleware

만약 Middleware를 모든 자원에 적용시키고 싶다면 main.tsuse 메서드를 추가하면 됩니다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './middlewares/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(logger);
  await app.listen(3000);
}
bootstrap();
주의: main.ts는 Function Middleware밖에 지원하지 않습니다.

만약 Class Middleware를 사용하고자 할 경우 AppModuleNestModule을 구현하고 라우팅 경로에 *을 추가해주면 됩니다.

 

마치며

Middleware의 사용 범위는 무척 넓습니다.

많은 애플리케이션은 Middleware에 기반하였습니다. ex: CORS

주의: cors를 비활성화 하는 설명은 추후에 다시 소개하겠습니다. 

오늘 배운 사항들을 복습해보며 이번 포스팅을 마치겠습니다.

 

1. Middleware는 라우팅 처리 전에 실행되는 함수입니다. 요청 객체와 응답 객체에 접근할 수 있습니다.

2. 미들웨어에는 Functional Middleware, Class Middleware 두가지의 설계 방식이 있습니다.

3. Module에서는 NestModule 인터페이스를 구현하고 configure() 메서드를 작성하여 MiddlewareConsumer를 통해 각각의 미들웨어를 관리할 수 있습니다.

4. 단일/다수의 Middleware를 단일/다수의 라우팅, HttpMethod 또는 Controller에 적용할 수 있습니다.

5. 특정 라우팅 경로를 제외하는것도 가능합니다. Middleware를 적용하지 않거나 exclude 메서드를 사용하면 됩니다.

6. Middleware를 전역에 적용할 수도있습니다.