๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
NestJS

[NestJS, TypeScript] Swagger API์— Generic Type ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•

by LasBe 2024. 11. 22.
๋ฐ˜์‘ํ˜•

๐Ÿ“’ [NestJS, TypeScript] Swagger์— Generic Type ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•


๋ฐฑ์—”๋“œ์—์„œ ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ผ์ •ํ•œ ํ…œํ”Œ๋ฆฟ์ด ์ •ํ•ด์ ธ ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

export class SuccessResponse<T> {
  @ApiProperty({ description: '์‘๋‹ต ๋ฐ์ดํ„ฐ' })
  payload: T;
  
  @ApiProperty({ description: '์ƒํƒœ์ฝ”๋“œ' })
  code: string;
  
  @ApiProperty({ description: 'ํ˜ธ์ถœ๋œ URI' })
  request: string;
}

 
์œ„ ํƒ€์ž…์€ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋ณด๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์ž…๋‹ˆ๋‹ค.
์ด๋Ÿฌํ•œ ์ค‘๋ณต๋˜๋Š” ํƒ€์ž…์„ ๊ฐ DTO๋งˆ๋‹ค ์‚ฌ์šฉํ•˜๋ฉด ๋ถˆํŽธํ•จ์ด ํ•œ๋‘ ๊ฐ€์ง€๊ฐ€ ์•„๋‹ˆ์–ด์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
 

@Get()
@ApiExtraModels(GetBrandListReq)
@ApiOkResponse({
  description: '๋ธŒ๋žœ๋“œ ๋ฆฌ์ŠคํŠธ',
  type: SuccessResponse<Brand[]>,
})
findByFilter(@Query() query: GetBrandListReq) {
  return this.brandService.findByFilter(query);
}

 

๊ทธ๋Ÿฐ๋ฐ ์›ฌ๊ฑธ? ์Šค์›จ์ด๊ฑฐ์— ํƒ€์ž…์ด ํ‘œ์‹œ๊ฐ€ ์•ˆ๋ฉ๋‹ˆ๋‹ค.
์ด์œ ๋Š” Swagger๊ฐ€ NestJS์˜ ๋Ÿฐํƒ€์ž„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฌธ์„œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐ, TypeScript์˜ ์ œ๋„ค๋ฆญ์€ ์ปดํŒŒ์ผ ๋‹จ๊ณ„์—์„œ ์†Œ๋ฉธ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
 

์ด๋ ‡๊ฒŒ ๋˜๋ฉด ํ™•์ธํ•˜๊ธฐ๋„ ๋ถˆํŽธํ•˜๊ฑฐ๋‹ˆ์™€, ํ”„๋ก ํŠธ์—์„œ swagger-typescript-api๋ฅผ ์‚ฌ์šฉํ•ด ํƒ€์ž…์„ ๊ธ์–ด์˜ฌ ๋•Œ๋„ ์ œ๋Œ€๋กœ ํ‘œ์‹œ๊ฐ€ ์•ˆ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“Œ Swagger Generic Type ํ‘œ์‹œํ•˜๊ธฐ

์ด๋Ÿฌํ•œ ํ˜„์ƒ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด getSchemaPath๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๊ฒ ์ง€๋งŒ, ์Šค์›จ์ด๊ฑฐ ๋ฌธ์„œํ™” ์ฝ”๋“œ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด์ง€๋Š” ๊ฒƒ์ด ๋ง˜์— ์•ˆ ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

import { mixin } from '@nestjs/common';

type Constructor<T = object> = new (...args: any[]) => T;

export class TypeUtil {
  static getSuccessResponse<T extends Constructor>(Base: T) {
    class Class extends SuccessResponse<T> {
      @ApiProperty({ type: Base, description: '์‘๋‹ต ๋ฐ์ดํ„ฐ' })
      payload: T;
    }
    return class SuccessResponse extends mixin(Class) {};
  }
  static getSuccessResponseList<T extends Constructor>(Base: T) {
    class Class extends SuccessResponseList<T> {
      @ApiProperty({ type: [Base], description: '์‘๋‹ต ๋ฐ์ดํ„ฐ' })
      payload: T;
    }
    return class SuccessResponseList extends mixin(Class) {};
  }
}
// dto
export class GetBrandListRes extends TypeUtil.getSuccessResponseList(Brand) {}
@Get()
@ApiExtraModels(GetBrandListReq)
@ApiOkResponse({
  description: '๋ธŒ๋žœ๋“œ ๋ฆฌ์ŠคํŠธ',
  type: GetBrandListRes,
})
findByFilter(@Query() query: GetBrandListReq) {
  return this.brandService.findByFilter(query);
}

 

๐Ÿ”Ž Mixin์„ ์ด์šฉํ•ด ์ƒˆ๋กœ์šด ํด๋ž˜์Šค ๋ฐ˜ํ™˜

mixin์€ ํด๋ž˜์Šค์˜ ์ƒ์†์ด๋‚˜ ํ™•์žฅ์˜ ๊ฐœ๋…์ด ์•„๋‹Œ ๋‘ ๊ฐœ ์ด์ƒ์˜ ํด๋ž˜์Šค๋ฅผ ์„ž์–ด ์ƒˆ๋กœ์šด ํ•˜๋‚˜์˜ ํด๋ž˜์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

import { mixin } from '@nestjs/common';

type Constructor<T = object> = new (...args: any[]) => T;

function mixinClass<T extends Constructor>(Base: T) {
  class Class extends Response<T> {
    property: T;
  }
  return class NewResponse extends mixin(Class) {};
}

์œ„ ์ฝ”๋“œ๋Š” TypeScript์—์„œ ๊ธฐ์กด ํด๋ž˜์Šค๋ฅผ ํ™•์žฅํ•˜๊ฑฐ๋‚˜ ๋™์ ์œผ๋กœ ์ƒˆ๋กœ์šด ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฏน์Šค์ธ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
๋•๋ถ„์— ์ปดํŒŒ์ผ ์‹œ์ ์— ์กฐํ•ฉ๋˜๋Š” ์ œ๋„ค๋ฆญ์„ ์šฐํšŒํ•ด ๋Ÿฐํƒ€์ž„์— ๋™์ ์œผ๋กœ ํด๋ž˜์Šค๊ฐ€ ํ•ฉ์ณ์ง€๊ฒŒ ๋˜์–ด ์Šค์›จ์ด๊ฑฐ์— ํƒ€์ž…์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€


์˜คํ”ˆ ์ฑ„ํŒ