query parameter로 JSON object 또는 배열 받기

김가영·2021년 11월 23일
1

Node.js

목록 보기
34/34

원래 query parameter와 path parameter는 기본적으로 key-value 형태로 이루어져있다. 주로 value로 이용되는 것은 number, string 등 기본 타입이다.

그렇다면, 기본 타입 이외에 다른 타입, 그 중에서도 JSON object나 배열을 query param의 값으로 받을 수는 없을까?

Nest.js(+Typescript)에서 JSON object를 쿼리 값으로 받게된 경위와 그 과정에서 겪은 어려움을 정리해봤다.

WHEN?

먼저 언제 query parameter로 JSON이나 배열을 받고 싶을까?

클라이언트에서 여러 요소 가운데에 정렬에 이용할 요소를 선택하는 경우를 생각해보자. 가장 간단한 방법은 기본적으로 제공하는 Key-value 형태를 이용하는 것이다. order_by 를 통해 어떤 요소로 정렬을 할 지를 나타냈고, order 를 통해 정렬 방식(오름차순)을 나타냈다.

GET /search?order=asc&order_by=name

또는, : 를 구분자로 이용하여 하나의 string으로 받은 후 서버에서 split(':') 와 같은 파싱을 거칠 수도 있을 것이다.

GET /search?order_by=name:asc

하지만 만약, 두가지 이상의 target을 이용하여 정렬을 하고 싶다면 어떻게 해야할까? 이름으로 정렬하면서, 이름이 같은 객체는 생년월일로 정렬하고 싶다면?

nestjs 내장 기능 이용하기

nestjs에서는 같은 query key를 중복해서 보낼 경우 자동으로 배열로 만들어주는 기능을 제공한다.

GET /search?order_by="name:asc"&order_by="birthDay:desc"
export class Dto {
  @Type(() => String)
  order_by!: string[];
}

이런 식으로 dto를 생성해주기만 하면 된다.

배열 이용하기

이 경우에도 request uri의 순서대로 order_by 요소가 정렬되지만, 보다 명확하게 순서를 드러내고 싶다면? 이 때 배열을 이용할 수 있다.

GET /search?order_by=["name:asc", "birthDay:desc"]

다만 dto는 아래와 같이 Transform 과정을 거쳐야한다.

export class Dto {
  @Transform(({ value }) => JSON.parse(value))
  @Type(() => String)
  query!: string[];
}

JSON object

이번에는 결과를 필터링 하고싶은 상황을 생각해보자. 특정 날짜 사이의 검색결과들 중 내가 좋아요한 것들만 불러오고 싶을 수 있다. 또는 내가 좋아요한 결과들 중 식료품 카테고리를 불러오고 싶을 수도 있다.

이런식으로 쿼리가 복잡해질수록 단순한 key-value 값을 가지는 쿼리로는 요구사항을 구현하기 힘들어진다. 이 때 json object를 이용하면 원하는 바를 보다 명확하게 표현할 수 있다.

{
	"createdAt": {
		"gt": "2020-12-01",
		"le": "2021-12-01"
	},
	"status": "liked"
}

{
	"or": {
		"status": "liked",
		"category": "grocery"
	}
}
export class QueryDto {
  @IsOptional()
  @Transform(({ value }) => JSON.parse(value))
  filter?: FilterDto;
}

JSON을 이용하여 or, greaterThan, lessThan 등 복잡한 연산을 요구할 수 있다. 하지만 실제로 이를 구현하기는 확실히 어려웠다. 어려움을 느꼈던 부분들을 정리해봤다.

JSON object as Query Param

URL Encoding

https://en.wikipedia.org/wiki/Query_string#URL_encoding

URL에 이용할 수 없는 character들이 있다. space와 같이 애초에 URL에서 이용할 수 없는 문자들(공백문자)이 있고, 이미 URL에서 특정 의미로 사용되고 있는 문자일 수도 있다(예를 들어 = ).

이를 해결하기 위한 방법이 바로 URL Encoding이다. URL Encoding은 특수문자들을 %HH 의 형태로 파싱하여 URL에서 이용할 수 없는 문자들을 이용하는 방법을 제공한다.

https://learning.postman.com/docs/sending-requests/requests/#sending-parameters

postman에서는 자동으로 parameter에 대한 URL-encoding을 진행하지는 않는다. 그렇기 때문에 Encode URI Component 를 따로 적용시켜줘야하는 것,

JSON을 parameter로 이용할 때에도 자동으로 {, }, " 와 같은 특수문자들을 이용하게 되기에 이러한 URL Encoding을 해주어야했다.

처음 Postman과 테스트에서 요청할 때 이 부분을 고려하지 못했다.

Postman에서는 Encode URI Component (우측클릭 → 메뉴 버튼 클릭)를 통해 encoding을 할 수 있고, 코드로 요청할 때에는 JSON.stringfy() 를 이용하면 해당 부분을 encoding을 할 수 있다.

Validation

가장 오랜 시간동안 고민한 문제였다. json query에 대한 nested validation이 진행되지 않았다.

export class FilterDto {
  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  tags?: string[];
}

export class QueryDto {

  @IsOptional()
  @Transform(({ value }) => JSON.parse(value))
  @ValidateNested()
  @Type(() => FilterDto)
  filter?: FilterDto;
}

사실 중점적인 이유는 테스트 파일의 validationPipe와 실제 main.ts의 validationPipe가 일치하지 않기 때문이었다. test code에서 validationError 대신 timeout이 계속 일어났다.(특히 @IsArray 데코레이터를 이용할 때 이런 timeout이 발생했다)

export interface ValidationPipeOptions extends ValidatorOptions {
  exceptionFactory?: (errors: ValidationError[]) => any;
}

validationPipe의 어떤 option이 영향을 끼친 것일까?

→ 확인해본 결과, exceptionFactory가 존재하지 않아서로 확인되었다.

exceptionFactory: Takes an array of the validation errors and returns an exception object to be thrown.

분명 optional한 option인데, 왜 없으면 작동이 안되는 걸까?

https://stackoverflow.com/questions/60270468/throw-same-error-format-as-class-validator-in-nestjs

비슷한 이슈

결국, json을 쿼리로 받을 경우 해결 방법은

export class FilterDto {
  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  tags?: string[];
}

export class QueryDto {

  @IsOptional()
  @Transform(({ value }) => plainToClass(GetFormFilterDto, JSON.parse(value)))
  @ValidateNested()
  @Type(() => GetFormFilterDto)
  filter?: GetFormFilterDto;
}

위와 같이 plainToClass 를 이용하면 되었다.

원래 IsString({each: true}) 는 각자가 string인지만 검사하기 때문에 배열 대신 { tags: "something" } 과 같은 요청이 들어와도 validationError를 던지지 않았다. 그래서 IsArray() 데코레이터를 추가해줬다.

profile
개발블로그

1개의 댓글

comment-user-thumbnail
2021년 12월 9일

잘봤어요~~

답글 달기