본문 바로가기

Backend/NestJS

NestJS - 대충 서비스 만들어보기 (11)

이번 포스팅에선 NestJS 의 미들웨어에 대해 간단히 보도록 하겠습니다.

 

- Middleware

 

NestJS 의 미들웨어는 원래 알고 있던 Express 의 미들웨어와 다르지 않습니다.

일반적으로 미들웨어라 함은 클라이언트의 요청에 맞는 응답을 하기 위한 중간에 거쳐가는 함수들이라고 볼 수 있습니다.

Express 에선 미들웨어라고 하면 대략 아래와 같이 구성되어 있는데요.

 

(req: Request, res: Response, next: NextFunction) {
  ...
  next();
}

 

req(요청) 와 res(응답) 객체 및 다음 미들웨어 함수의 액세스 권한을 갖는 함수를 말하며 next 를 호출함으로써 다음 미들웨어로

현재 요청을 넘길 수 있고, 이러한 구성에 의해 미들웨어는 순차적으로 처리됩니다.

이러한 정의때문에 next 가 없으면 미들웨어라고 하지 않습니다. (그런 경우엔 그냥 Controller 겠죠)

 

Express 미들웨어에 대한 문서에는 아래와 같은 일들을 미들웨어에서 수행할 수 있다고 되어있습니다.

 

  • 코드 수행 및 요청, 응답 객체에 대한 변경
  • 요청, 응답 라이프 사이클 종료
  • next 를 사용해 다음 미들웨어에 제어권 전달

라이프 사이클을 끝낸다는건 미들웨어에서도 응답을 보내거나 (res.json 등의) 에러 처리를 해야 하는데, 이런 행위를 하지 않으면서

next 도 호출하지 않으면 해당 어플리케이션은 아무것도 할 수 없는 행에 걸리게 됩니다.

 

Express 미들웨어에 대해 얘기하고 있는 것 같지만 NestJS 미들웨어도 그냥 이것과 같다고 보시면 됩니다.

우리는 미들웨어를 통해 쿠키나 본문 파싱, 세션 관리, Auth 와 같은 것들을 처리할 수 있습니다. 다만 NestJS 에서 Auth 는 Guards

사용하라고 권장합니다. 이 내용은 아마 다음 포스팅에서 적어볼 것 같습니다.

 

현재 팀에선 NestJS 앱에서 미들웨어를 적극적으로 쓰고 있진 않으나 Express 로 구성된 앱에선 주로 요청 객체에 대한 변경을

미들웨어에서 가져가고 있습니다. query 또는 body 로 들어온 파라미터를 사용해 어떠한 메타데이터를 담은 객체를 구성하고 그것을

요청 객체에 담아 넘김으로써 이후 로직에선 해당 객체를 사용한 코드만 구성합니다.

이러한 커스텀 미들웨어를 잘 구성하면 관심사를 좀 더 잘게 분리하고 비즈니스 로직에만 집중할 수 있는 장점이 있습니다.

 

NestJS 는 사실 뭔가 다 미들웨어처럼 보이는 것들을 전부 나눠놨습니다. 이전에 봤던 파이프를 비롯해 미들웨어, 인터셉터, 위에서 얘기한 가드, 예외 필터가 있죠. 얘네들에선 차차 보면서 잘 분류해보도록 하겠습니다.

 

- 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.info('Request!');
    next();
  }
}

 

src 하위에 logger 디렉토리를 만들고 logger.middleware.ts 파일을 생성해 위와 같이 작성해주었습니다.

계속 적어왔던 것처럼 Express 미들웨어와 동일하게 (req, res, next) 로 구성되어있죠.

NestMiddleware 를 위와 같이 구현해주면 됩니다. 별다른게 없이 Reqeust! 를 찍고, 다음 미들웨어로 next 를 통해 넘깁니다.

 

import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './logger/logger.middleware';
...

@Module({
  ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('/ads');
  }
}

 

app.module.ts 를 위와 같이 변경해줍니다. NestModule 을 구현함으로써 미들웨어를 모듈에 포함시킬 수 있습니다.

ads 경로로 들어오는 모든 요청에 대해서 작성했던 LoggerMiddleware 를 통하게 만들었습니다.

이제 한번 localhost:3000/ads 경로로 요청을 보내 확인을 해보죠.

 

 

Request! 로그가 찍힌것을 볼 수 있습니다. 너무 간단하네요.

비슷한 코드를 좀 더 추가해 요청 객체를 변경해보도록 하겠습니다.

 

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.info('Request!');
    (req as any).locals = { test_local: 1 };
    next();
  }
}

@Injectable()
export class LoggerMiddleware2 implements NestMiddleware {
  use(req: any, res: any, next: NextFunction) {
    console.info('Request2!', req.locals);
    next();
  }
}

 

아까 작성했던 logger.middleware.ts 에서 LoggerMiddleware2 를 추가해줍니다.

그리고 LoggerMiddleware 에선 req 객체에 locals 라는 프로퍼티를 추가해줬습니다.

 

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware, LoggerMiddleware2).forRoutes('/ads');
  }
}

 

미들웨어는 순차적으로 실행된다고 했었죠. NestJS 에서 미들웨어 등록은 위처럼 넣어주면, 넣은 순서대로 동작합니다.

이제 다시 localhost:3000/ads 에 요청을 보내보겠습니다.

 

 

LoggerMiddleware2 가 받은 요청 객체에 locals 가 세팅된 것을 확인할 수 있습니다.

 

- Etc

 

그 외엔 좀 부가적인 옵션들을 보겠습니다.

 

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware, LoggerMiddleware2)
      .exclude({ path: '/ads/:id', method: RequestMethod.GET })
      .forRoutes('/ads');
  }
}

 

위에선 forRoutes 메서드를 통해 /ads 경로에 대해 전부 적용을 했었는데, 특정 path 는 제외할 수 있는 메서드를 exclude 로 제공합니다.

대체로 미들웨어는 그 특성상 어떠한 경로만 제외할 일은 잘 없지만 혹시나 필요하면, 이런 메서드를 활용할 수 있습니다.

 

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

export function loggerMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  console.info('Request function!');
  next();
}

 

위에선 주입 가능한 프로바이더로써 미들웨어를 생성하고 특정 경로에 해당 미들웨어를 적용하는 방법을 사용했었는데,

전역으로 적용하는 방법을 보도록 하겠습니다. logger.middleware.ts 파일에서 기존에 작성했던건 다 지우고, 위처럼 작성합니다.

app.module.ts 에도 LoggerMiddleware 와 LoggerMiddleware2 관련된 것들은 전부 지워줍니다.

 

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    }),
  );
  app.use(loggerMiddleware);
  await app.listen(3000);
}
bootstrap();

 

전역으로 설정하기 위해선 main.ts 에서 app.use 에 넣어줘야합니다. 이렇게 세팅한 후 다시 요청을 보내 확인해보죠.

 

 

참고로 만약 전역으로 설정한 미들웨어와 기존 미들웨어를 같이 사용했다면, 전역 미들웨어가 먼저 동작하게 됩니다.

당장 미들웨어를 통해 뭐 할건 없어서 간단하게 봤지만, 커스텀 미들웨어를 작성해 관심사를 잘 분리하는 것은 결국 

개발자 각각의 몫인 것 같습니다. 보이기 위해 넣었던 미들웨어들은 전부 제거하고, Auth 와 관련해 가드를 사용하는 내용을

다음 포스팅에서 보도록 하겠습니다.