자바스크립에서 쿼리스트링 포맷/파싱하기 - 2

joonseokhu·2020년 8월 15일
2

이번엔 객체로 객체를 쿼리스트링으로 바꿔보자

const data = {
  myId: 1,
  myName: 'John Doe',
  message: 'Hello, I\'m John Doe.'
}

이런 객체를 쿼리스트링으로 변환해야 하는 상황이라고 하자

결과는 아마 이렇게 될 것이다.

myId=1&myName=John Doe&message=Hello, I'm John Doe.

먼저 객체를 배열로 바꿔야 한다.

기본적으로 자바스크립트에서 객체는 반복문에 사용하지 못하는데, 순서를 가지지 않기 때문이다. 하지만 우리가 원하는 결과가 순서가 크게 중요하지 않다면 임의로 순서를 부여해 반복가능하도록 만들 수 있다.

쿼리스트링은 순서를 가진것처럼 보이지만 사실은 임의로 직렬화 된 것 뿐, 일반적으로 순서를 보장하지 않는 프로토콜이다. 즉 아래 두개의 쿼리스트링은 같은것으로 취급해도 괜찮다.

message=Hello, I'm John Doe.&myName=John Doe&myId=1
myId=1&myName=John Doe&message=Hello, I'm John Doe.

때문에 우리는 순서에 대해 신경안쓰고 쿼리스트링으로 변환할 객체를 반복시킬것이다.

for-in 문이나 for-of 문으로 객체 반복이 가능하지만, 배열로 변환한 후 사용하는게 깔끔하다. Object.entries 라는 정적 메서드를 사용하자.

정적 메서드란?

생성자/클래스에 직접 달려있는 메서드를 말한다.
자바스크립트 기본생성자로 예를 들자면
[].map(e => e) 에서 map 메서드는 어떤 배열에 내장된 메서드이다.
하지만 Array.isArray([]) 에서 isArray 는 Array 라는 배열 생성자에만 들어있는 메서드이다.
여기서 .map 은 배열의 인스턴스 메서드 라고 하고, .isArray 는 배열의 정적 메서드이다.

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  console.log(result)
}

콘솔은 이렇게 이렇게 나온다

[
  [ 'myId', 1 ],
  [ 'myName', 'John Doe' ],
  [ 'message', "Hello, I'm John Doe." ]
]

2차원 배열 안의 각 배열에서 왼쪽 요소는 키, 오른쪽 요소는 값이다. = 로 연결하면 된다.
2차원 배열을 map 으로 반복시키면서 각 요소를 변환하면 된다.
배열에서 파싱함수를 만들때 했듯이, 메서드 체이닝으로 작성해보자.

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  	.map(([key, value]) => `${key}=${value}`)

  console.log(result)
}

그러면 이제 이렇게 된다.

[ 'myId=1', 'myName=John Doe', "message=Hello, I'm John Doe." ]

각 배열요소를 & 로 연결하자

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  	.map(([key, value]) => `${key}=${value}`)
	.join('&')
  console.log(result)
}

결과는 다음과 같다.

myId=1&myName=John Doe&message=Hello, I'm John Doe.

쿼리스트링 결과물이 존재하면, 쿼리스트링 구분기호인 '?'를 맨 앞에 붙이는 기능도 추가하고,
코드를 정리해서 마무리하자.

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  	.map(([key, value]) => `${key}=${value}`)
	.join('&')
  return result ? `?${result}` : ''
}

비표준 uri 문자로 인한 문제

하지만 이걸 그대로 쓰면 문제가 생긴다. 키/값으로 사용되는 문자열중에, 띄어쓰기문자, 따옴표, 마침표와 같은 기호는 uri에서 의미를 부여하는 기호들과 혼동을 일으킬 수 있다.

예를들어, 쿼리스트링으로 '&' 라는 기호가 포함된 문자를 값으로 전달하려고 한다고 치자

{ dangerousValue: '&&&&' }

이걸 우리가 만든 함수를 이용해서 쿼리스트링으로 변환한다면, 이렇게 될 것이다.

?dangerousValue=&&&&

이 쿼리스트링이 포함된 url을 우리가 저번에 만든 쿼리스트링 파싱 함수를 이용해 객체로 다시 변환하면 이렇게 된다.

{ dangerousValue: '', '': undefined }

기호 '&'는 쿼리스트링에서, 키-값 쌍들을 연결하는 특수한 의미를 가진 기호이기에, 이상하게 처리되는 문제가 발생한다.

하지만 그렇다고 해서, 쿼리스트링에서 사용가능한 문자의 종류를 제한하기엔, 쿼리스트링의 사용성이 너무 제한된다.

그래서, uri에는, 이런 상황에 특수한 몇몇 문자를 이스케이프 하는 규칙이 존재한다.
브라우저용 자바스크립트에도 이를 위한 api가 내장되어있으므로, 사용해보도록 하자.

쿼리스트링 포맷 함수 수정

encodeURIComponent 는 uri에서 표준적인 문자로 취급하지 않는 문자들을 찾아 이스케이프 처리한 결과를 리턴하는 브라우저 내장함수다.

encodeURIComponent('안녕, 나는 John & Doe 42.')
--> %EC%95%88%EB%85%95%2C%20%EB%82%98%EB%8A%94%20John%20%26%20Doe%2042.

작성했던 쿼리스트링 포맷 함수를 수정해보자

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  	.map(([key, value]) => `${key}=${value}`)
	.join('&')
  return result ? `?${result}` : ''
}

이중, 키와 값은 경우에 따라 쿼리스트링에서 지원하지 않는 문자가 포함될 수 있으므로, 해당부분을 수정하면 된다.

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  	.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
	.join('&')
  return result ? `?${result}` : ''
}

또는 이렇게도 작성 가능하다.

const stringifyQueryString = (data) => {
  const result = Object.entries(data)
  	.map((e) => {
      const [key, value] = e.map(encodeURIComponent)
      return `${key}=${value}`
    })
	.join('&')
  return result ? `?${result}` : ''
}

쿼리스트링 파싱

저번에 만들었던 파싱함수도, uri 이스케이프처리에 대응하도록 다시 작성해보자.

const parseQueryString = (url) => {
  const [_, query] = url.split('?')
  if (!query) return {}
  return query.split('&').reduce((acc, e) => {
    const [key, value] = e.split('=')
    return { ...acc, [key]: value }
  }, {})
}

파싱할때는 decodeURIComponent 를 쓰면 된다. 마찬가지로 key, value 둘다 적용하자

const parseQueryString = (url) => {
  const [_, query] = url.split('?')
  if (!query) return {}
  return query.split('&').reduce((acc, e) => {
    const [key, value] = e.split('=').map(decodeURIComponent)
    return { ...acc, [key]: value }
  }, {})
}

마무리

이제 기본적인 쿼리스트링 포맷/파싱 기능을 구현했다.
하지만 모든 기능을 구현한 것은 아니다.

값이 평범한 문자열일땐 괜찮지만, 값이 undefined이거나 null이거나, 빈문자열일수도 있다.
또한 값이 객체나 배열인 상황도 존재한다.

두루 쓰이는 라이브러리들은 이런 상황에 잘 대응되어 있다. 우리가 만든 함수를 실무상황에서 쓰기엔 어렵다.

하지만, 자바스크립트를 막 배우는 사람들이나 신입 개발자들은 이렇게 기본적인 기능을 가진 함수 작성도 어려워 하는 경우를 많이 보았기 때문에 이 글을 작성하게 되었다.

아주 간단해 보이는 문제이지만, 객체, 배열, 문자열에 대한 어느정도의 이해도를 요구하는 기능이며, 나아가 웹개발에서 아주 기본적이면서 핵심적인 지식인 uri에 대한 이해가 있어야 한다.

자신이 이제 웹개발을 막 시작한 개발자라면 한번쯤 쿼리스트링 처리 함수를 여러 방법으로 만들어보는 것도 성장하는데 도움이 될 것 같다.

profile
풀스택 집요정

1개의 댓글

comment-user-thumbnail
2020년 8월 16일

잘보고 갑니다!

답글 달기