스웨거에서 같은 코드의 응답은 예시를 여러개를 넣지를 못한다.
위처럼 기술한경우 하나만 적힌다.
또한 응답 예시 (Example Value ) 를 어느 경우엔 어떤 응답이 온다고 알려주고 싶으면
content 부분에 예시응답 객체를 직접 생성해서 넘겨줘야하는데
이럴경우 모든값을 다 생성해줘서 넘겨주어야한다.
이러면, 그냥 공통부분빼고 바뀌는 부분만 알려주고 싶은데 , 나머지값들 까지 예시값으로 다 적어줘야하니 작성하기 어려움이 있다.
이러한 문제점들을 해결하기 위해
@ApiProperty 로 적은 정보들을 메타데이터로 빼오고
새로운 객체를 만드는 과정을통해
@SuccessResponse , @ErrorResponse 커스텀 데코레이터를 만들것이다.
예시가 적으면 프론트가 이해하기에 어려움이 있을 수도 있기때문에 아래gif처럼 되는 기능을 만들어볼 것이다.
@SuccessResponse , @ErrorResponse 커스텀 데코레이터 기능정리
- 같은 응답코드에 여러 응답 스키마를 보여줄 수 있다.
- 같은 응답코드에 여러 응답 예시를 보여줄 수있다.
- 예시를 적을때에 바꾸고 싶은 값만 적으면 그부분만 바뀌어서 예시를 보여줄 수 있다.
- 공통 응답 부분 까지 보여 줄 수있다.
- 성공응답의경우 페이지네이션응답처럼 제네릭 타입 까지 지원해줘야한다.
현 글내용은 1편으로 Dto class에 적어준 @ApiProperty 로 메타데이터정보를 가져와서
description 값 이나 example 값을 떼와서 객체로 만드는 방법을 알아볼 것이다.
이방식을 알아야 예시값을 위처럼 넘겨줄 수 가있다.
만든 예시값들은 아래처럼 content를 활용해서 넘겨줄거다.
content가 뭔지는 네스트 문서말고 스웨거 가서 봐야지 알 수 있다.
요거까지 체크하고 넘어가긴 양이넘많고 공식문서 가서 보길 권장한다.
중요한 뽀인트는 examples 가 값이 있는 객체 상태여아한다는 것이다.
1편 글을 통해서 얻어갈 수있는 부분들
- @ApiProperty 관련 메타데이터 키값 알아오기
- 디티오 클래스에서 @ApiProperty 가 적힌 필드정보 가져오기
- 각 필드정보에서 @ApiProperty에 기술한 정보 가져오기
- 가져온 정보를 바탕으로 객체 만들기
- type 이 다른 디티오 클래스 일경우 재귀 호출해서 만들기
- lazyType () => {} 형태일 때 타입 꺼내오기
- Array 타입일때 예시값을 어레히 형으로 만들어주기
이 내용은 음. 어렵다. 본인도 때려맞추는 식으로 작성한 코드들이다. 참고해주시길 바란다
이글에 나와있는 코드들은 고스락 티켓 에매 프로젝트에서 사용중인 코드들 이다 전체 소스는 아래에서 확인 할 수있다.
@ApiProperty 동작방식에 대한 이해
네스트가 @ApiProperty 데코레이터를 통해서 우리가 적어줬던 정보들을 스키마나 , 예시 객체로 바꿔 줄 수있는 건
메타데이터를 이용하기 때문이다
메타데이터 관련해서 궁금하신분들은 타입스크립트 공식문서 찾아보시길 바란다.
우선 nest swagger 레포에서 ApiProperty 를 어떻게 만들고 있는지 한번 살펴보도록하자.
@ApiProperty 데코레이터를 만드는 소스 부분이다.
@ApiProperty({ description: '유저정보들.', type: [UserProfileDto] })
위처럼 적어주면 안에 적어준 정보들을 가지고 type 정보나 어레이 형인지, 좀더 부가적으로 처리할 부분을 처리하고
공통으로 createPrpeortyDecorator로 넘기고있다.
( 중요 46번째줄에 getEnumType 때문에 value가 숫자형 이넘이면 nubmer 로 value가 스트링이면 string으로 적히는데 밑에서 한번 다시 언급하겠다)
createPrpeortyDecorator 에보면 중요한 포인트가 두가지다.
Reflect.defineMetaData 로 두가지의 키값으로 값을 저장하고있다
DECORATORS.API_MODEL_PROPERTIES_ARRAY
우선 @ApiProperty 데코레이터는 클래스의 필드에만 적을 수 있는 데코레이터인데,
갑자기 properties 라고 값을 가져온다. 값을 가져오면서 43번째 줄에서 ...properties 하면서 :propertyKey 형식으로 새로운 값을 API_MODEL_PROPERTIES_ARRAY 키값에 넣어 주고있다.
근데 Reflect.defineMeatdat 41번째 줄을 보면 target 만 넣고있다.
이건 클래스 레벨로 메타데이터를 정해준 것이라고 생각하면된다.
우린 나중에 API_MODEL_PROPERTIES_ARRAY 메타데이터 키를 이용해서 디티오 클래스의 @ApiProperty 를 적어준 필드들의 목록을 뽑아와서 생성자 없이 객체를 만들어 반환하는데 쓸것이다.
DECORATORS.API_MODEL_PROPERTIES
이건뭐 각 필드별로 메타데이트럴 정해준것이다. defineMetadata 할때 4번째인자로 propertyKey 값을 넘겨줘서 가져올 땐
Reflect.getMetadata(API_MODEL_PROPERTIES,dtoClass.prototype,fieldName);
뭐 이렇게 꺼내줄거다.
nestjs의 스웨거 의 코드를 뜯어보면서 우리는 두가지 정보를 얻었다.
- @ApiProperty 관련 메타데이터 키값 알아오기
- 각 메타데이터 키값에서 어떤 정보를 저장하고 있는지.
API_MODEL_PROPERTIES_ARRAY 는 디티오 클래스에 @ApiProperty 를 적어줬던 필드들의 키값 목록
API_MODEL_PROPERTIES 는 @ApiProperty를 적은 각필드들에 인자로 넘겨준 type 같은 정보들
를 저장하고있다.
이제 꺼내오는 방법에 대해서 알아보자.
위 두 캡쳐 소스는 아래에 링크 걸어놨다.
@ApiProperty 메타데이터를 통해 객체만들기
다시 무얼 만들지 리마인드 하면,
스웨거에 예시응답을 보여줄려면
class 타입이아니라 class타입으로 생성한 객체 ( 생성자가 있는 객체 ) ,
타입만 같은 객체 ( plain object ) 를 어떻게든 만들어서 값으로 넘겨줘야한다
코드를 보여주기전에 말로 좀 먼저 풀자면...
- API_MODEL_PROPERTIES_ARRAY 를 통해서 클래스에 @ApiProperty 적힌 필드목록뽑아오기
- API_MODEL_PROPERTIES 를 통해서 필드의 ApiPropertyOptions 정보 빼오기
- 빼온 정보가지고 타입만 같은 순수 객체 만들어서 반환하기
마지막에 순수 객체를 만들어서 반환한다는 이야기는
타입스크립트는 참.... 생성자 하나밖에 못만든다. ㅋ
생성자 오버라이딩 지원안해준다.
new PageDto() --> 바로그냥 오류남
그래서 생성자를 통해서 객체 생성을 해버리면 생성자에 필요 인자가 있을시에 오류가 나버린다
어떤 느낌이냐면 스프링 jpa entity 만들때 기본생성자 프로텍트로 만드는 느낌? 이다. 결국 뒤에서 메타데이터 가지고 뭘할려면 기본생성자는 하나 필요하다 이야기다.
위는 자바 세상이야기고 사실 자바스크립트는 클래스의 인스턴스 만 있는 것이아니라 순수 객체라는 것이 있다.
클래스의 인스턴스라고 부르는것도 어찌보면 매우 부적절하지만 이렇게해야좀 이해하기 편해서 이렇게 부르겠다.
그냥 클래스가 사실 얹혀진 느낌이고 자바스크립트는 고냥 객체세상에 생성자 정보가 있냐 없냐의 차이일 뿐이다.
우린 얻어온 필드정보들을 통해 객체를 직접만들어서 반환해 줄거다
헬퍼 펑션, 타입들
mport { ApiPropertyOptions } from '@nestjs/swagger';
// 스웨거 메타데이터 키
const DECORATORS_PREFIX = 'swagger';
const API_MODEL_PROPERTIES = `${DECORATORS_PREFIX}/apiModelProperties`;
const API_MODEL_PROPERTIES_ARRAY = `${DECORATORS_PREFIX}/apiModelPropertiesArray`;
//nest js 에서 사용중인 Type ( 생성자 )객체
export interface Type<T = any> extends Function {
new (...args: any[]): T;
}
// source form lodash 오브젝트 인지 체킹
function isObject(value) {
const type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
// 기본생성자 인지 체크하느 ㄴ함수
function isFunction(value): value is Function {
if (!isObject(value)) {
return false;
}
return true;
}
// () => type 형태의 순환참조로 기술했을때 가져오는 함수
function isLazyTypeFunc(
type: Function | Type<unknown>
): type is { type: Function } & Function {
return isFunction(type) && type.name == 'type';
}
// 원시타입인지 확인
function isPrimitiveType(
type:
| string
| Function
| Type<unknown>
| [Function]
| Record<string, any>
| undefined
): boolean {
return (
typeof type === 'function' &&
[String, Boolean, Number].some(item => item === type)
);
}
// Type 인지 확인하는 커스텀 타입 체커
function checkType(object: any): object is Type {
return object;
}
// ApiPropertyOption에 필드네임까지 추가해서 타입정의
type ApiPropertyOptionsWithFieldName = ApiPropertyOptions & {
fieldName: string;
};
맨위에 키값들은 nestjs 가 메타데이터 키값을 저장하는 실제 const 값이다.
그리고 타입체깅하는 함수들은 propertyType이 타입이 저렇게나 무식하게 많기 때문에 가지치기를 좀 해줘야한다...
가지치기를 위해서 필요한 타입체킹 함수들이다.
소스는 뭐 네스트꺼 참고하면서 만들기도하고, 적당하게 짜집기 한것도 있다.
아그리고 메타데이터 키값으 const는 아래에서 가져왔당
makeInstanceByApiProperty 구현
makeInstanceByApiProperty 의 구현 요구사항을 먼저 정리하고 가겠다.
- 디티오 클래스 타입을 인자로 넘겨주면 @ApiProperty에 적은 값들을 통해서 객체를 만들어 넘겨준다
- @ApiProperty 의 type 으로 적어준 정보를 토대로 형식에 맞게 값을 반환해야한다.
- generic 타입에 대해서 대응 할 수 있어야한다.
- type 이 따른 Dto 클래스이면 재귀적으로 호출해서 field : dtoclassinstance 형태로 반환해야한다.
- type 이 따른 Dto 클래스 array 형이면 재귀적으로 호출해서 field : [dtoclassinstance] 형태로 반환해야한다.
- type 이 따른 Dto 클래스에 순환참조를 방지위해 Lazy 형이이도 받아올 수 있어야한다.
export function makeInstanceByApiProperty<T>(
dtoClass: Type,
generic?: Type
): T {
// 디티오로 생성자를 만들지 않고 해당 타입만 가져옴.
// 생성자에 인자가 들어간경우 에러가 남.
const mappingDto = {};
// metadata 에서 apiProperty로 저장했던 필드명들을 불러옴
const propertiesArray: string[] =
Reflect.getMetadata(API_MODEL_PROPERTIES_ARRAY, dtoClass.prototype) || [];
// apiProperty로 적었던 필드명 하나하나의 정보를 가져오기 위함
const properties: ApiPropertyOptionsWithFieldName[] = propertiesArray.map(
field => {
// :fieldName 형식이라서 앞에 : 를 짤라줌 위에 createProperty nest 꺼 참조해보면 : 붙임
const fieldName = field.substring(1);
// 각 필드마다 메타데이터를 가져옴
const obj = Reflect.getMetadata(
API_MODEL_PROPERTIES,
dtoClass.prototype,
fieldName
);
obj.fieldName = fieldName;
return obj;
}
);
// mappingDto 를 만듬 함수형으로 적을까 했는데 ... for문 돌렸습니다
for (const property of properties) {
const propertyType = property.type;
// property.type apiproperty에 type 을 기술 안 할 수 있으므로 undefiend 체크
if (propertyType) {
// 이건 커스텀임 generic을 위한 커스텀
// dto에 T 제네릭으로 들어가는게 있다면 type을 generic 으로 적어주세요
if (propertyType === 'generic') {
// generic으로 만들 추가적인 타입이 있다면
if (generic) {
// array 형이면 [] 안에 담아서 재귀 호출
if (property.isArray) {
mappingDto[property.fieldName] = [
makeInstanceByApiProperty(generic)
];
} else {
// 오브젝트형이면 그냥 바로 호출
mappingDto[property.fieldName] = makeInstanceByApiProperty(generic);
}
}
} else if (propertyType === 'string') {
// 스트링 형태의 enum
if (typeof property.example !== 'undefined') {
mappingDto[property.fieldName] = property.example;
} else {
mappingDto[property.fieldName] = property.description;
}
} else if (propertyType === 'number') {
// 숫자형태의 enum
if (typeof property.example !== 'undefined') {
mappingDto[property.fieldName] = property.example;
} else {
mappingDto[property.fieldName] = property.description;
}
} else if (isPrimitiveType(propertyType)) {
// 원시타입 [String, Boolean, Number]
if (typeof property.example !== 'undefined') {
mappingDto[property.fieldName] = property.example;
} else {
mappingDto[property.fieldName] = property.description;
}
} else if (isLazyTypeFunc(propertyType as Function | Type<unknown>)) {
// type: () => PageMetaDto 형태의 lazy
// 익명함수를 실행시켜 안에 Dto 타입을 가져옵니다.
const constructorType = (propertyType as Function)();
if (Array.isArray(constructorType)) {
mappingDto[property.fieldName] = [
makeInstanceByApiProperty(constructorType[0])
];
} else if (property.isArray) {
mappingDto[property.fieldName] = [
makeInstanceByApiProperty(constructorType)
];
} else {
mappingDto[property.fieldName] =
makeInstanceByApiProperty(constructorType);
}
} else if (checkType(propertyType)) {
//마지막 정상적인 클래스 형태의 타입
if (property.isArray) {
mappingDto[property.fieldName] = [
makeInstanceByApiProperty(propertyType)
];
} else {
mappingDto[property.fieldName] =
makeInstanceByApiProperty(propertyType);
}
}
}
}
return mappingDto as T;
}
src/common/utils/makeInstanceByApiProperty.ts
한부분 한부분 뜯어보도록 하겠다
디티오를 리터럴 객체로 선언하고 있다.
dtoClass : type ( type은 헬퍼 타입이다 . 네스트에서 공통으로 사용하고있음 Function을 생성자 타입으로 쓰지말라고 하더라고요..)
으로 인자를 받아오기 때문에 new dtoClass() ; 로 할수도 있으나
생성자가 이미 존재한다면 에러가 발생한다.
API_MODEL_PROPERTIES_ARRAY키값을통해서 클래스의 필드 명들을 가져온다.
클래스자체의 메타데이터를 가져올려면 왼쪽처럼 (API_MODEL_PROPERTIES_ARRAY )
클래스의 필드 정보를 가져올로면 오른쪽 처럼 하면된당! (API_MODEL_PROPERTIES)
필드명들을 가져온후에 맵을 돌면서 각 필드들의 메타데이터들을 가져오면서 필드네임도 추가해준다 ( 나중에 편하게 코드를 작성할려고 이다.)
type -> 제네릭
페이지 네이션 공통 디티오처럼 제네릭으로 가져오면 문제가있다.
<T> 표현으로 실제 타입 값을 주고받는게아니고 , T 는 그저.. 컴파일 타임에 타입스크립트에서 도움 되라고 ide 에서 막아주는 용도다
실제로 data : T[] 에서 뭐 프로퍼티를 꺼내고 그럴수가없다.
그래서 최소한의 하나의 제네릭을 지원하기위해서 인자로 generic 타입을 받고 ,
property 정보에서 타입정보를 빼내오는 것이 아닌. generic 타입으로 재귀 콜을 해준다.
이때 isArray : true 면 예시값을
아래와같이 [] 형으로 만들어주기위해서 적어준다.
특이점은 @ApiProperty({isArray : true , type : Dto }) 나 @ApiProperty({ type : [Dto]}) 나 둘이똑같이
isArray 가 true 로 나온다.
이런식으로 mappinDto.제네릭이들어간필드 = 들어갈 예시값 으로 저장한다.
isArray 는 위에 createApiPrpeortyDecorator 함수를 보면 어레인지 구분하고 isArray true,false 를 정해져서 넘겨오는 정보이다
enum -> 스트링 , 넘버
잠시 위에올라가서 createPrpeortyDecorator 를 보면,
이넘 밸류가 글자면 string , 숫자면 number로 반환하고 있다
이값들은 property.type 이고
실제로 property 를 로그 찍어보면 이렇게 나온다...
어쩔수없나보다 처리해줘야지!
예시값이 적혀저있으면 예시값을 우선으로 아니면 description을 땡겨오게 했다.
여기는 다른 클래스 타입이아니라 실제 값이므로 그대로 반환해줬다.
type : 원시타입 [String , Boolean, Number]
위 사진처럼 원시타입 일경우도 지원해준다. 저 세가지 형태로 그대로 값으로 집어넣어준다.
type : Lazy func
위처럼 type : () => 형태로 순환참조 방지를 위해 레이지 하게 선언된경우
type 이 그냥 () => Dto 요런 형태로 나온다.
익명함수니 실행시켜서 꺼내오면된다.
그리고 type : () => [Dto] 같은 극악 무도한 경우도 있으니
익명함수 실행시켰을때 어레이형인지 함확인하고 isArray인지도 확인하면서 재귀 콜을 햇다.
type : DtoClass
type : UserProfileDto 인경우 위처럼 나온다.
마찬가지로 어레이인지 확인후 재귀콜로 땡겼다.
위처럼 이렇게 커스텀 타입이면 재귀콜을 때려서 원시타입이 나올때까지 뎁스가 들어간다.
그러면 이제 위처럼 공통 응답 디티오에셔 예시객체를 빼오거나 디티오에서 예시객체를 빼오는 작업을 진행할 수 있다.
이상으로
makeInstanceByApiProperty 함수를 통해서 내가 적어놨던
@ApiProperty 정보를 가지고 descrition 또는 example 값을 가지고 새로운 객체를 만들어서 반환 받을 수 가있다.
이를 이용해서 이제 예시값을 적어줄수 있다.
+ 메타데이터로 값 가져오는건 문제가 아니였는데
타입체킹하는게 진짜 너무 힘들었다.. ㅠ
다음 포스팅에서는! 저렇게 만들어진 예시 응답값으로 스웨거 문서를 적는 데코레이터를 만들거다!
2편 성공응답 데코레이터 만들기
'nest.js' 카테고리의 다른 글
[고스락 티켓 2.0] nest js swagger 같은 코드 여러 응답 예시 만들기 (3) - 에러응답 데코레이터 만들기 (0) | 2022.08.16 |
---|---|
[고스락 티켓 2.0] nestjs swagger 같은 코드 여러 응답 예시 만들기 (2) - 성공응답 데코레이터 만들기 (1) | 2022.08.15 |
[고스락 티켓 2.0] nestjs db rollback repository 테스트 (0) | 2022.08.14 |
[고스락 티켓 2.0] nestjs transaction with repository (0) | 2022.08.14 |
[고스락 티켓 2.0] nestjs redis forRootAsync 모듈 만들기 (0) | 2022.08.13 |