본문 바로가기

node js/NestJs

[Nest Js] Nest Js 공식 문서 파헤치기 - Middleware(미들웨어)

트리스티가 Nest Js를 공부하며 남긴 기록입니다. 틀린 내용은 언제든지 말씀해주세요 ~!

 


지난 포스팅에서는 Modules가 무엇인지를 알아보았습니다. 이번 포스팅에서는 Middleware가 뭔지 알아보도록 하겠습니다. 

 

 

 

 

📣 Middleware란 무엇인가?

 

미들웨어(Middleware)는 Nest Js가 아니라 express를 사용하셨던 분들이라면 한 번쯤은 써보셨을 기능으로, 라우트 핸들러 이전에 호출되는 함수입니다. 라우트 핸들러는 쉽게 말하면 클라이언트에서 url을 통해 서버의 엔드포인트에 접근할 때 올바른 엔드포인트를 호출할 수 있게끔 해주는 핸들러라고 생각하시면 됩니다.

 

미들웨어는 요청(request) 및 응답(response) 객체 및 어플리케이션 요청 - 응답 주기 중간에서 그다음의 미들웨어 함수에 대한 액세스 권한을 갖으며, 목적에 맞게 작업을 수행하는 역할을 담당합니다. 밑의 그림처럼 client가 올바른 엔드포인트를 찾기 위해 라우트 핸들러를 사용하기 이전에 미들웨어가 어떤 작업을 처리해준다고 생각하시면 됩니다.

 

출처: https://docs.nestjs.kr/middleware

 

 

 

Nest Js의 미들웨어는 기본적으로 express 미들웨어와 동일한 기능을 수행합니다. Nest Js에서는 함수 또는 클래스를 미들웨어로 만들어 줄 수 있습니다. 함수는 미들웨어로 만들기 위해서 특별한 조건이 없으나, 클래스의 경우에는 service를 만들어줄 때와 마찬가지로 @Injectable() 데코레이터가 있어야 하고 NestMiddleware 인터페이스를 구현해야 합니다. 클래스 미들웨어를 간단하게 만들어주고 싶다면 nest cli 명령어를 사용해 쉽고 빠르게 만들어 줄 수 있습니다. 만약 study 미들웨어 클래스를 만들어주려면 nest g mi study라고 입력하시면 됩니다.

 

// study.middleware.ts

@Injectable()
export class StudyMiddleware implements NestMiddleware {
	use(req: any, res: any, next: () => void) {
		console.log("hello middleware");
		next();
	}
}

 

 

Nest Js는 미들웨어의 의존성 주입도 지원하기 때문에 providers나 controller와 마찬가지로 동일한 모듈내의 providers나 controller에서 constructor를 사용하여 의존성을 주입해줄 수 있습니다.

 

 

 

 

📣 next()  

 

여기서 주의 사항이 있는데요. 미들웨어를 만들다가 "StudyMiddleware에 req, res는 사용하지 않는데 제거해도 괜찮지 않을까?"라는 생각을 하실 수도 있습니다. 그래서 한번 없애보고 실행을 해보도록 하겠습니다.

 

 

// study.middleware.ts

@Injectable()
export class StudyMiddleware implements NestMiddleware {
	use(next: () => void) {
		console.log('hello middleware');
		next();
	}
}

 

 

보시는 바와 같이 next is not a finction 오류가 발생하게 됩니다. 보통 미들웨어는 다음과 같은 작업을 수행하게 됩니다.

 

● 모든 코드를 실행
● 요청 및 응답 객체를 변경
● 요청-응답주기를 종료
● 스택의 next 미들웨어 함수를 호출
● 현재 미들웨어 함수가 요청-응답주기를 종료하지 않으면 next()를 호출하여 next 미들웨어 기능에 제어를 전달

 

여기서 미들웨어 함수가 요청-응답 주기를 종료하지 않으면 next()를 호출하여 next 미들웨어 기능에 제어를 전달이라는 부분이 중요한데요. req, res가 없는 경우라면 해당 미들웨어 함수가 next() 메소드를 호출하여 다음 미들웨어에 기능을 전달할 수가 없게 됩니다. 따라서 next() 메소드는 정상 작동을 하지 못하게 돼서 오류를 발생시키게 됩니다. 

 

미들웨어를 작성할 때는 req, res, next()는 반드시 필요하다는 것을 알아두세요!

 

 

 

 

 

📣 Applying Middleware  

 

여기서 한 가지 의문점이 생길 수 있습니다. @Module() 데코레이터를 사용해서 만들어진 모듈 클래스에는 미들웨어를 위한 속성이 존재하지 않습니다. 그러면 어떻게 해야 모듈에 미들웨어를 등록할 수 있을까요? 미들웨어의 경우에는 @Module() 데코레이터의 속성을 사용하지 않고 모듈 클래스에 configure() 메소드를 사용하여 설정할 수 있습니다. 모듈에 미들웨어를 등록하기 위해서는 NestModule 인터페이스를 구현해야 합니다.

 

저는 StudyModule에 미들웨어를 등록해보았습니다. 아래의 코드는 'study'라고 라우팅 경로가 설정된 라우팅 핸들러에 대해 StudyMiddleware를 정의하는 코드입니다. 제 StudyController의 @Controller() 데코레이터에 'study'라고 경로가 설정되어 있기 때문에 StudyController로 접근하기 위해서는 StudyMiddleware를 거치게 됩니다.

 

// study.module.ts

@Global()
@Module({
	controllers: [StudyController],
	providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule implements NestModule {
	configure(middlewareExam: MiddlewareConsumer) {
		middlewareExam.apply(StudyMiddleware).forRoutes('study');
	}
}

 

 

 

만약 'study'라고 설정된 라우팅 핸들러에 접근할 때 그중에서 특정 HTTP 메소드로 접근하게 하고 싶으면 다음과 같이 설정할 수도 있습니다. 저는 @Get() 데코레이터에 접근할 때 StudyMiddleware를 거치도록 설정해보았습니다.

 

// study.module.ts

@Global()
@Module({
	controllers: [StudyController],
	providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule implements NestModule {
	configure(middlewareExam: MiddlewareConsumer) {
		middlewareExam
			.apply(StudyMiddleware)
			.forRoutes({ path: 'study', method: RequestMethod.GET });
	}
}

 

 

 

코드를 실행해서 'study' 라우팅 경로에 접근하여 Get 통신을 하면 아래와 같은 결과가 나타납니다. id와 name을 쿼리 파라미터로 입력하면 그대로 반환하는 엔드포인트에 접근하면 StudyMiddleware에 작성한 console.log()가 작동하게 됩니다.

 

 

 

 

📣 Route wildcards  

 

forRoutes() 메소드의 path 속성은 패턴 기반 라우팅도 지원하기 때문에 아래와 같이 와일드카드를 사용할 수도 있습니다. 다만, fastify의 경우에는 더 이상 와일드카드의 '*'을 지원하지 않는 path-to-regexp 패키지를 사용하기 때문에 (.*), :splat* 등의 매개 변수를 사용해야 합니다.

 

// study.module.ts

@Global()
@Module({
	controllers: [StudyController],
	providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule implements NestModule {
	configure(middlewareExam: MiddlewareConsumer) {
		middlewareExam
			.apply(StudyMiddleware)
			.forRoutes({ path: '*', method: RequestMethod.GET });
	}
}

 

 

 

📣 MiddlewareConsumer  

 

모듈 클래스에 미들웨어를 선언하기 위해서는 NestModule를 구현하고 configure()메소드를 사용해야 합니다. configure() 메소드 안에는 MiddlewareConsumer라는 것이 있는데 이것은 Nest js에서 제공해주는 헬퍼 클래스(helper class)입니다. 헬퍼 클래스는 특정 클래스의 작업을 도와주는 클래스로 다양한 기능들을 제공해 줍니다.

 

MiddlewareConsumer의 forRoutes() 메소드는 위에서 미리 보셨듯이 문자열을 사용할 수도 있고, controller 클래스를 사용할 수도 있고, RouteInfo 객체를 넣을 수도 있습니다. 대부분의 경우에는 controller 클래스를 전달하거나 쉼표를 사용해서 여러 개의 controller 클래스를 전달합니다. 만약 StudyModule의 StudyMiddleware를 사용하려고 할 때 StudyController에만 접근하게 하려면 다음과 같이 설정할 수 있습니다.

 

// study.module.ts

@Global()
@Module({
	controllers: [StudyController],
	providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule implements NestModule {
	configure(middlewareExam: MiddlewareConsumer) {
		middlewareExam.apply(StudyMiddleware).forRoutes(StudyController);
	}
}

 

 

apply() 메소드도 forRoutes() 메소드가 하나의 controller와 연결되지 않듯이 하나의 단일 미들웨어를 연결하거나, 여러 미들웨어를 연결할 수도 있습니다.

 

 

 

 

📣 Excluding routes  

 

그렇다면 특정 라우팅 경로를 제외하고 싶을 때는 어떻게 설정해야 할까요? 이럴 때는 exclude() 메소드를 사용하여 특정 라우트를 쉽게 제외하여 내가 원하는 경로에만 접근할 수 있게 설정할 수 있습니다. exclude() 메소드도 forRoutes() 메소드처럼 와일드카드를 사용해서 설정할 수 있지만, path-to-regexp 패키지를 사용하여 와일드카드 매개 변수를 지원하기 때문에 해당 패키지의 문법에 작성하셔야 합니다. 아래의 예제는 StudyController의 'study' 경로에 있는 POST를 제외한 나머지 요청 메소드에 대해서만 StudyMiddleware를 적용합니다. 

 

// study.module.ts

@Global()
@Module({
	controllers: [StudyController],
	providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule implements NestModule {
	configure(middlewareExam: MiddlewareConsumer) {
		middlewareExam
			.apply(StudyMiddleware)
			.exclude({ path: 'study', method: RequestMethod.POST })
			.forRoutes(StudyController);
	}
}

 

 

 

 

 

📣 Functional routes  

 

 

이번에는 모듈 클래스가 아니라 모듈 함수를 사용해보도록 하겠습니다. 아까 모듈 함수의 경우에는 특별한 조건이 없어도 된다고 말씀드렸습니다. 만약 모듈에서 미들웨어를 등록할 때 여러 개의 미들웨어를 등록하고 순차적으로 실행하게 하려면 apply() 메소드에 쉼표를 사용하여 작성해주시면 됩니다.  

 

다음은 모듈 함수를 만들고 등록하는 코드입니다. apply에 StudyMiddleware, StudyFuinctionMiddleware 순으로 작성하였기 때문에 미들웨어 작동 순서도 이와 같은 순서로 실행됩니다.

 

// studyFuinction.middleware.ts

export function StudyFuinctionMiddleware(
	req: Request,
	res: Response,
	next: NextFunction
) {
	console.log('hello function middleware');
	next();
}


// study.module.ts

@Global()
@Module({
	controllers: [StudyController],
	providers: [StudyService, ChildService, TestServiceA]
})
export class StudyModule implements NestModule {
	configure(middlewareExam: MiddlewareConsumer) {
		middlewareExam
			.apply(StudyMiddleware, StudyFuinctionMiddleware)
			.exclude({ path: 'study', method: RequestMethod.POST })
			.forRoutes(StudyController);
	}
}

 

 

 

 

📣 Global middleware  

 

미들웨어를 등록된 모든 라우팅 경로에 한 번에 적용되게 하려면 INestApplication 인스턴스에서 제공하는 use() 메소드를 사용해서 전역 미들웨어로 등록할 수 있습니다. 밑의 코드는 조금 전에 작성한 StudyFuinctionMiddleware을 전역 미들웨어로 만드는 코드입니다.

 

// main.ts

async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	app.use(StudyFuinctionMiddleware);
	await app.listen(3000);
}
bootstrap();