nest.js

[고스락 티켓 2.0] nestjs swagger 같은 코드 여러 응답 예시 만들기 (2) - 성공응답 데코레이터 만들기

ImNM 2022. 8. 15. 16:13

 

 

[고스락 티켓 2.0] nestjs swagger 같은 코드 여러 응답 예시 만들기 (1) - @ApiProperty로 객체 만들기

스웨거에서 같은 코드의 응답은 예시를 여러개를 넣지를 못한다. 위처럼 기술한경우 하나만 적힌다. 또한 응답 예시 (Example Value ) 를 어느 경우엔 어떤 응답이 온다고 알려주고 싶으면 content 부

devnm.tistory.com

1편과 연결되는 내용이다.

서론은 생략하겠따.!

 

 

이글을 통해서 얻을 수있는 점들

 

  • 같은 코드에 여러 성공응답 예시 작성하는방법
  • 예시로 적고싶은 부분만 적어서 예시의 일정부분만 바꾸는 방법
  • SuccessResponse 데코레이터 만들기

 

 

이글에서 쓰인 코드는 고스락 티켓예매 프로젝트에서 사용중인 코드이다

SuccessResponse 코드는 아래에서 확인가능하다.

 

GitHub - Gosrock/Ticket-Backend-22nd

Contribute to Gosrock/Ticket-Backend-22nd development by creating an account on GitHub.

github.com

 

 

ApiResponse examples


 

 

How can I display multiple ResponseDTOs' schemas in Swagger/NestJS?

I have this route which can return one of these two different DTOs: @Get() @ApiQuery({ name: 'legacy', description: "'Y' to get houses legacy" }) async findAllHouses( @Query('l...

stackoverflow.com

 

 

위에스택오버플로우에 보면은

 

요론소스가 나오는데

주의할점은 value에 클래스 타입을 적으면 안먹힌다.

value에서도 볼수있듯이 스웨거 공식문서보면 저 value라는 값은 실제 값을 가지고있는 객체가 들어가야한다.

 

 

 위그림과같이 examples 안에 "예시1" 라는 필드는 examples 에서 예시 1 , 2 를 고르는 셀렉 버튼이되고,

안에 value 와 description 은 각각 example value , example descritption 이된다.

 

주의할점은 value의 객체 참조 주소가 달라야한다. 안그럼 똑같다고 판단해버린다..! value 주소가같으면 둘중 하나는 사라진다.

 

앗근데! example value 오른쪽에 있는 schema가 없다!

스키마 정보도 추가해보쟈

 

 

ApiResponse schema


 

 

[Feature Request] Multiple response with the same status code · Issue #225 · nestjs/swagger

I'm submitting a... [ ] Regression [ ] Bug report [x] Feature request [ ] Documentation issue or request [ ] Support request => Please do not submit support request here, instead post your q...

github.com

위 링크 들어가서보면 ㅋㅋ 멀티플 응답 any update? 그런다 ... 우리가 지금 이렇게 만드는 이유이다

 

스키마를 등록할려면 $ref 로 클래스의 위치가 어딨는지 가져와야한다 ( 이것도 뭐 네스트말고 기본 스웨거 공식문서에 오브젝트 가져오는 방법이다 이해 안되면꼭 가서보시길) 가져올때 루는 추가적으로 한작업을 더해줘야하는데 

ApiExtraModles 를 통해서 디티오 모델을 등록을 시켜줘야한다 

네스트 공식문서에도 이부분은 있긴함! ApiExtraModel 쳐보세욤

 

이렇게 extraModel 을 등록하고 , oneof로 스키마까지 깔꼼하게 보여줬다.

근데 이렇게 매번 api 메소드앞에다 달껀아니구 깔끔하게 만들어줄거다.

 

여기서 이제 고민이 생기는 부분이다..
  • 예시값은 어떻게 ... 저렇게 매번 적어줘야하는거야...? 하나 응답일땐 type만 적어줘도 네스트가 자동으로 만들어줬잖아!!!
  • 예시값중에 일부분만 바뀐걸로 보여주고싶은데... 꼭 매번 예시하나 보여줄려고 객체를 다만들어야하나.?
  • 잠만 우리 공통 응답부분은 예시값으로 안보여줘요?
  • 페이지네이션은 어떻게 하죠...? Dto 안에 제네릭 있는경우는요?!?!?

 

1. 예시값을 만드는건 앞편에서 makeInstanceByApiProperty 함수로 객체를 만드는 방법으로 하면된다.

2. 일부분만 바뀐건 바꿀 값만 받아서 1.의 방법으로 객체를 만든뒤 머지하는 오버라이트를 하면된다.

3. 공통응답도 1.의 방법으로 만들면 된다.

4. 우리 제네릭 지원하자나 ㅋ

 

 

SuccessCommonResposeDto.ts


import { HttpStatus } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { EnumToArray } from '../utils/enumNumberToArray';

export class SuccessCommonResponseDto<T> {
  @ApiProperty({ enum: EnumToArray(HttpStatus), description: '상태코드' })
  @Expose()
  readonly statusCode: number;

  @ApiProperty({ type: Boolean, description: '성공여부' })
  @Expose()
  readonly success: boolean;

  @ApiProperty({
    type: 'generic',
    description: 'object 또는 array 형식의 응답데이타가 옵니다.'
  })
  @Expose()
  data: T;
}

src/common/dtos/SuccessCommonResponse.dto.ts

 

고스락 티켓 예매 프로젝트에서는 클라이언트로 응답값 떤져줄때 success인터셉터를 통해서 공통응답을 제공해주고있고

data 부분이 각 디티오마다 바뀌는 부분이다.

(1)편에서 봤듯이 generic 으로 타입을 지정해주면, makeInstanceByApiProperty 함수를 통해서 객체를 만들 때 generic 인자로 제네릭 모델 타입을 넘겨주면 data필드부분에 해당 타입으로 객체를 만들어서 리턴해준다.

 

위와같은 공통응답은 어플리케이션 전반에 걸쳐서 제공하므로 따로 뭐 안빼고, 그냥 우리가 만들 데코레이터 부분에 심을것이다.

이제 소스를 보도록 하자.

 

SuccessResponse.decorator.ts


실제 사용할 모습은 위와같다. 여기에 우린 페이지 네이션 까지 지원한다면

generic 까지 끼워넣어서 작성할수있다. PageDto 의 리스트 부분도 T 형 제네릭이당.

 

 

 

자 이제 소스 들어가보자

interface SuccessResponseOption {
  /**
   * 응답 디티오를 인자로받습니다
   * 예시 : ResponseRequestValidationDto
   */
  model: Type<any>;
  /**
   * 예시의 제목을 적습니다
   */
  exampleTitle: string;
  /**
   *  깊은 복사로 변경하고 싶은 응답값을 적습니다. 오버라이트 됩니다.
   *  nested 된 obj 인 경우엔 해당 obj 가 바뀌는것이아닌 안에 있는 property만 바뀝니다.
   *  즉 주어진 객체로 리프 프로퍼티에 대해 오버라이트됩니다.
   */
  overwriteValue?: Record<string, any>;
  /**
   * 어떠한 상황일 때 예시형태의 응답값을 주는지 기술 합니다.
   */
  exampleDescription: string;
  /**
   * 제네릭 형태가 필요할 때 기술합니다.
   * pageDto<generic> 인경우?
   */
  generic?: Type<any>;
}

src/common/decorators/SuccessResponse.decorators.ts , SuccessResponOption interface

 

우선은 인자로 받은 인터페이스를 보자

Type  타입은 Function 생성자인데 타입스크립트측에서 쓰지말라해서 nest 에서 공통적으로 쓰는 타입을 가져왔다.

나머지 오버라이트 밸류는 객체를 변경할 부분만 받아서 덮어씌우기 를 진행할거다.

 

generic? 은 페이지네이션 같은 디티오엔에 또 제네릭이 있을경우 집어넣는다. 한층만 지원해준다 ... 제네릭 안에 제네릭안에 제네릭까진 지원못해준다.... 욕심버려라!

 

 

 return applyDecorators(
    // $ref를 사용하기 위해선 extraModel 로 등록 시켜야한다.
    ApiExtraModels(...extraModel, ...extraGeneric, SuccessCommonResponseDto),
    ApiResponse({
      status: StatusCode,
      content: {
        'application/json': {
          schema: {
            // 베이스 스키마
            additionalProperties: {
              $ref: getSchemaPath(SuccessCommonResponseDto)
            },
            // dto 스키마들
            oneOf: [...pathsOfDto, ...pathsOfGeneric]
          },
          // 예시값
          examples: examples
        }
      }
    })
  );

src/common/decorators/SuccessResponse.decorators.ts

 

우선은 이런형태의 커스텀 데코레이터를 만들어서 반환할꺼다

applyDecorators는 네스트에서 지원해준다.

extraModle, 없을수도있는 extraGenric 타입, 그리고 우리의공통응답 dto를 ExtraModel로 등록해주고,

베이스 스키마부분엔 공통응답을 나머지 oneOf엔 dto 스키마들

example 부분엔 우리가 map 함수를 통해 예시를 만들어서 반환해줄꺼다. value와 description 있고

value에는 makeInstanceByApiProperty 함수를 통해서 예시 객체를 만들면서 , 오버라이팅 할거를 하고 집어넣을꺼다.!

 

 

const examples = succesResponseOptions
    .map((response: SuccessResponseOption) => {
      // base CommonResponse 를 만듭니다.
      const commonResponseInstance = makeInstanceByApiProperty<
        SuccessCommonResponseDto<any>
      >(SuccessCommonResponseDto);

      const DtoModel = response.model;

      // dto 객체를 만든다. 제네릭은 옵셔널 한 값이라 없으면 없는대로 만든다.
      const dtoData = makeInstanceByApiProperty<typeof DtoModel>(
        DtoModel,
        response.generic
      );
      // overWriteValue가 있으면 오버라이트
      // 정보를 좀더 커스텀 할 수있다.
      if (response.overwriteValue) {
        commonResponseInstance.data = mergeObjects(
          {},
          dtoData,
          response.overwriteValue
        );
      } else {
        commonResponseInstance.data = dtoData;
      }

      // 예시 정보를 만든다 ( 스웨거의 examplse)
      return {
        [response.exampleTitle]: {
          value: commonResponseInstance,
          description: response.exampleDescription
        }
      };
    })
    .reduce(function (result, item) {
      Object.assign(result, item);
      return result;
    }, {}); // null 값 있을경우 필터링

  // 스키마를 정의 내리기 위한 함수들
  const extraModel = succesResponseOptions.map(e => {
    return e.model;
  }) as unknown as Type[];
  // 중복값 제거
  const setOfExtraModel = new Set(extraModel);
  // $ref 추가
  const pathsOfDto = [...setOfExtraModel].map(e => {
    return { $ref: getSchemaPath(e) };
  });
  // 제네릭 관련
  const extraGeneric = succesResponseOptions
    .map(e => {
      return e.generic;
    })
    .filter(e => e) as unknown as Type[];
  const pathsOfGeneric = extraGeneric.map(e => {
    return { $ref: getSchemaPath(e) };
  });

src/common/decorators/SuccessResponse.decorators.ts

 

우린 같은 코드에 여러 응답을 받을꺼니깐 어레이형으로 SuccessResponseOption을 받고.

map. -> reduce 함수형으로 돌면서 Object.assign 계속 값을 추가 해 나갈것이다. ( 아마 얕은복사일텐데 뭐 한번쓰고 말거니 넘어가자)

 

 

 

공통 응답부분을 먼저 예시 객체로 만든다.

아 보니깐 응답코드 오버라이트를안해줬다. 200번대 아니면 따른 값 넣도록 하자, 상태코드 인자로받으니 집어넣기만 하면된다.

이때 제네릭은 따로 안적어줬는데 따로data 로 만들어서 집어넣어줄꺼다.

 

제네릭넘겨주고  ( null 값이면 makeInstanceByApiProperty에서 알아서 처리를한다 )

오버라이트 할 값이 있으면 mergeObject 함수를 통해서 깊은 복사, 덮어씌기를 진행한다.

후에 공통응답 dto 객체에 data에 값을 넣은다.

 

export const mergeObjects = <T extends object = object>(
  target: T,
  ...sources: T[]
): T => {
  if (!sources.length) {
    return target;
  }
  const source = sources.shift();
  if (source === undefined) {
    return target;
  }

  if (isMergebleObject(target) && isMergebleObject(source)) {
    Object.keys(source).forEach(function (key: string) {
      if (isMergebleObject(source[key])) {
        if (!target[key]) {
          target[key] = {};
        }
        mergeObjects(target[key], source[key]);
      } else {
        target[key] = source[key];
      }
    });
  }

  return mergeObjects(target, ...sources);
};

const isObject = (item: any): boolean => {
  return item !== null && typeof item === 'object';
};

const isMergebleObject = (item): boolean => {
  return isObject(item) && !Array.isArray(item);
};

src/common/utils/mergeTwoObj.ts

 

깊은복사하는 머지 코드이다.. 스택오버플로우 에서 따왔다.

 

 

나머지는 map,reduce을통해서 example 객체형태로 만들고, 스키마 정보를 적어준다.

 


 

이렇게 되면 깔끔한 성공 응답을 만들수있따!

 젤 맘에드는 부분은 오버라이팅 기능이다 예시객체만들때 바꾸고 싶은 부분만 적어주면 되니,,, 얼마나 편한가!

실력이 된다면.. 컨트리뷰트를 하고싶은데 진짜 소스 보면 rxjs기반에,,, 이해가 안되는 부분들이 엄청많다

 

아무튼 이렇게 성공 응답 데코레이터를 

메타데이터를 활용해 @ApiProperty 가 적힌 필드들의 정보를빼와서

예시객체를 만든뒤에 여러 example을 적을 수 있는

커스텀하게 만들어 보았다.

 

다음글은  에러응답 데코레이터 가보자고!