Skip to Content
문서핵심 개념TypedMiddleware

TypedMiddleware

TypedMiddleware는 ASAPJS 미들웨어 시스템의 핵심입니다. 일반적인 Express 미들웨어가 단순히 요청을 처리하는 함수인 것과 달리, TypedMiddleware는 라우트 옵션을 받아 미들웨어를 생성하는 팩토리 구조를 가집니다.

이 구조를 통해 두 가지 강력한 기능을 제공합니다:

  • 런타임: 라우트별 옵션(auth: true 등)에 따라 동적으로 동작하는 미들웨어 생성
  • 컴파일타임: 미들웨어가 요구하는 옵션과 제공하는 컨텍스트를 핸들러까지 자동으로 타입 추론

TypedMiddleware 타입 구조

TypedMiddleware는 @asapjs/types에 다음과 같이 정의되어 있습니다.

// @asapjs/types export type TypedMiddleware< RouteOptions extends Record<string, any> = {}, Context extends Record<string, any> = {} > = ((options: RouteOptions) => (req: any, res: any, next: any) => void) & { readonly __contextType?: Context; readonly __errors?: any[] | ((options: RouteOptions) => any[]); };
  • RouteOptions: @Get, @Post 등 데코레이터 옵션에 추가될 필드 정의 (예: auth?: boolean)
  • Context: 미들웨어가 req 객체에 첨부하여 핸들러에 전달할 데이터 타입 (예: { user: JwtUserPayload })
  • __errors: 이 미들웨어가 throw할 수 있는 에러 목록. defineMiddleware 헬퍼가 자동으로 부착합니다.

TypedMiddleware 만들기 — defineMiddleware 헬퍼

커스텀 미들웨어를 만들 때는 defineMiddleware 헬퍼를 사용합니다. TypedMiddleware 타입을 직접 선언하는 것보다 에러 선언, 타입 강제 등의 기능을 통합적으로 제공합니다.

import { defineMiddleware } from '@asapjs/router'; export const myMiddleware = defineMiddleware< { myOption?: boolean }, // RouteOptions: 데코레이터에서 사용할 옵션 { myContext: string } // Context: 핸들러에서 사용할 데이터 >( ({ myOption = false } = {}) => (req, res, next) => { (req as any).myContext = myOption ? 'Option is enabled' : 'Option is disabled'; next(); } );

defineMiddleware(factory, meta?) 함수 시그니처:

인자설명
factory(options: RouteOptions) => (req, res, next) => void — 미들웨어 팩토리 함수
meta.errors이 미들웨어가 throw할 수 있는 에러 (선택). 배열 또는 함수 형태 지원

미들웨어에서 에러 선언하기

defineMiddlewaremeta.errors 옵션을 사용하면, 이 미들웨어가 등록된 모든 라우트의 Swagger 문서에 해당 에러들이 자동으로 포함됩니다.

배열 형태 — 항상 포함

import { error } from '@asapjs/error'; const RateLimitExceeded = error(429, 'RATE_LIMIT_EXCEEDED', '요청 한도를 초과했습니다', {}); export const rateLimitMiddleware = defineMiddleware<{}, {}>( () => (req, res, next) => { /* ... */ }, { errors: [RateLimitExceeded], // 이 미들웨어가 적용된 모든 라우트에 포함 } );

함수 형태 — RouteOptions에 따라 동적 결정

라우트 옵션에 따라 에러별로 포함 여부를 개별 제어할 수 있습니다.

import { error } from '@asapjs/error'; import { defineMiddleware } from '@asapjs/router'; const AuthErrors = { NO_TOKEN: error(403, 'NO_TOKEN_PROVIDED', '토큰이 제공되지 않았습니다', {}), INVALID_SIGNATURE: error(403, 'INVALID_TOKEN_SIGNATURE', '유효하지 않은 토큰 서명입니다', {}), UNAUTHORIZED: error(401, 'UNAUTHORIZED', '인증이 만료되었거나 유효하지 않습니다', {}), }; export const jwtMiddleware = defineMiddleware< { auth?: boolean }, { user: JwtUserPayload } >( ({ auth = false } = {}) => (req, res, next) => { // auth: false이면 검증 스킵 if (!auth) return next(); // JWT 검증 로직 ... }, { // 함수 형태: auth:true인 라우트에만 에러를 Swagger에 포함 errors: (options) => options.auth !== false ? [AuthErrors.NO_TOKEN, AuthErrors.INVALID_SIGNATURE, AuthErrors.UNAUTHORIZED] : [], } );

이렇게 정의한 미들웨어를 전역으로 등록하면:

@Get('/profile', { auth: true }) // → Swagger에 401(UNAUTHORIZED), 403(NO_TOKEN_PROVIDED), 403(INVALID_TOKEN_SIGNATURE) 자동 포함 @Get('/public', { auth: false }) // → 미들웨어가 실행되지 않으므로 Swagger에 에러 없음

Config에 등록하기

작성한 미들웨어를 defineRouterConfig에 등록하고, 프레임워크의 전역 타입을 확장(Augmentation)하여 타입 시스템을 연결합니다.

// src/config.ts import { defineRouterConfig } from '@asapjs/router'; import type { InferMiddlewareRouteOptions } from '@asapjs/router'; import { myMiddleware } from './middleware/myMiddleware'; export const routerConfig = defineRouterConfig({ middleware: [myMiddleware], }); export default { router: routerConfig, // ... }; declare module '@asapjs/router' { // 1. ExecuteArgs에 Context 타입 추가 interface GlobalMiddlewareContext { myContext: string; } // 2. IOptions에 RouteOptions 타입 추가 interface GlobalRouteOptions extends InferMiddlewareRouteOptions<typeof routerConfig.middleware> {} }
  • defineRouterConfig(): 미들웨어 배열의 튜플 타입을 보존하여 InferMiddlewareRouteOptions가 정확히 추론되게 합니다.
  • GlobalMiddlewareContext: ExecuteArgs의 기본 컨텍스트 타입입니다. 이를 확장하면 모든 핸들러에서 해당 필드를 타입 안전하게 사용할 수 있습니다.
  • GlobalRouteOptions: IOptions를 확장합니다. InferMiddlewareRouteOptions를 사용하면 등록된 모든 미들웨어의 옵션 타입이 자동으로 합산됩니다.

컨트롤러에서 사용하기

등록이 완료되면 데코레이터 옵션에서 미들웨어 옵션을 사용할 수 있고, 핸들러 인자에서 컨텍스트를 바로 꺼낼 수 있습니다.

import { ExecuteArgs, Get, RouterController } from '@asapjs/router'; class MyController extends RouterController { @Get('/example', { myOption: true, // ← GlobalRouteOptions에 의해 타입 체크됨 }) public handler = async ({ myContext }: ExecuteArgs) => { // myContext: string ← GlobalMiddlewareContext에 의해 자동 추론됨 return { message: myContext }; }; }

여러 미들웨어 조합

여러 개의 TypedMiddleware를 등록하면 각 미들웨어가 요구하는 옵션과 제공하는 컨텍스트가 자동으로 합쳐집니다.

export const routerConfig = defineRouterConfig({ middleware: [authMiddleware, tenantMiddleware], }); declare module '@asapjs/router' { interface GlobalMiddlewareContext { user: JwtUserPayload; // authMiddleware가 제공 tenantId: string; // tenantMiddleware가 제공 } interface GlobalRouteOptions extends InferMiddlewareRouteOptions<typeof routerConfig.middleware> {} // = { auth?: boolean } & { requireTenant?: boolean } }

동작 원리 (런타임)

TypedMiddleware는 요청 시점이 아닌 라우트 등록 시점에 팩토리가 호출됩니다.

  1. 설정 로드: config.router.middleware에 등록된 팩토리 목록을 확인합니다.
  2. 라우트 등록: 컨트롤러의 registerRoutes()가 호출될 때, 각 라우트의 IOptions를 모든 팩토리에 전달합니다.
  3. 미들웨어 생성: 각 팩토리는 옵션에 맞는 실제 Express 미들웨어를 반환합니다.
  4. 체인 구성: 반환된 미들웨어들이 Express 라우트 핸들러 앞에 순서대로 삽입됩니다.

이 방식은 매 요청마다 옵션을 체크하는 오버헤드를 없애고, 라우트 등록 시점에 최적화된 미들웨어 체인을 구성하게 해줍니다.


관련 항목

Last updated on