Logo

쿼리 스트링을 다루기 위한 자바스크립트 라이브러리 2종 비교 (qs vs. query-string)

웹 개발을 할 때 쿼리 스트링(Query String)을 다루는 일은 번거롭기도 하고 버그도 생기기 쉬워서 오래전부터 라이브러리가 많이 쓰였는데요. 관련해서 npm에서 검색해보면 이름이 조금씩 다른 수많은 라이브러리가 있어서 어떤 것을 선택해야 할지 헷갈릴 수 있습니다.

이번 포스팅에서는 현재 npm trends 기준으로 가장 많이 사용되고 관리가 잘 되고 있는 2종의 자바스크립트 라이브리리, qsquery-string에 대해서 알아보겠습니다.

라이브러리가 필요한 이유

먼저 자바스크립트로 쿼리 스트링을 다룰 때 왜 라이브러리가 필요한지 간단하게 짚고 넘어가겠습니다.

소위 검색 파라미터(search parameters)라고도 불리는 쿼리 스트링은 URL에서 경로(pathname) 바로 다음에 나오는 ? 기호로 시작하는 문자열인데요. 클라이언트에서 서버로 보통 필터(filter), 페이지네이션(pagination), 정렬(sort)을 위한 간단한 데이터를 보내기 위해서 사용됩니다.

쿼리 스트링은 ?key1=value1&key2=value2&... 형태로 여러 개의 키와 값의 쌍을 & 기호로 구분하여 매개변수를 명시하는데요. 매개변수의 개수가 많아지면 사람의 눈으로 읽기가 쉽지 않고 매개변수에 다국어나 특수 문자가 포함되어 있으면 인코딩/디코딩도 신경을 써줘야 합니다.

게다가 URL 명세에 따르면 쿼리 스트링은 ?key=value1&key=value2와 같이 동일한 키에 여러 개의 값을 할당하는 것도 허용하는데요. 이 부분을 처리하는 게 상당히 까다로울 수 있으며 경계 조건을 잘 고려하지 않으면 버그로 이어지기 쉽습니다.

그래서 이렇게 다루기가 까다로운 문자열 형태의 쿼리 스트링을 다루기 쉬운 객체의 형태로 변환해놓는 것이 유리한데요. 쿼리 스트링을 읽을 때도 객체로 변환해놓으면 키를 통해서 값에 쉽게 접근할 수 있고, 쿼리 스트링을 만들 때도 직접 문자열을 만들기 보다는 우선 객체로 만들어 놓은 담에 문자열로 바꾸는 것이 편합니다.

라이브러리를 사용하면 이러한 쿼리 스트링의 형태 변환을 한 번의 함수 호출로 처리할 수 있어서 매우 편리합니다.

qs

먼저 살펴볼 라이브러리는 qs는 쿼리 스트링을 다루기 위해서 정말 오랫동안 사용된 라이브러리인데요. 무려 13년의 역사를 자랑하며 다른 라이브러리 대비 패키지 이름이 짧아서 눈에 잘 띄기도 합니다.

qs는 npm을 통해서 설치할 수 있고요.

$ npm i qs

이 패키지를 사용할 때는 모든 함수를 몽땅 하나의 네임스페이스로 불러오는 경우가 많습니다.

import * as qs from "qs";

자바스크립트로 쿼리 스트링을 다룰 때 가장 빈번하게 필요한 작업은 문자열 형태의 쿼리 스트링을 객체의 형태로 변환하는 것일텐데요. 이 때는 qsparse() 함수를 사용합니다.

const obj = qs.parse("mode=dark&active=true&nums=1&nums=2&nums=3");
console.log(obj);
콘솔
{
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
}

반대로 자바스크립트 객체 형태의 쿼리 스트링을 문자열로 변환하고 싶을 때는 stringify() 함수를 사용합니다.

const str = qs.stringify({
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
});
console.log(str);
콘솔
"mode=dark&active=true&nums%5B0%5D=1&nums%5B1%5D=2&nums%5B2%5D=3"

그런데 걊이 배열인 nums 파라미터를 변환한 부분이 뭔가 굉장히 복잡하게 변형이 되었는데요. 이 것은 qs 라이브러리는 기본적으로 배열 값을 파라미터명[인덱스]=값의 형태로 변환하기 때문입니다.

웹 표준 방식으로 동일한 파라미터명이 반복되게 하고 싶다면, 두 번째 인자로 arrayFormat 옵션의 값을 "repeat"로 주시면 됩니다.

const str = qs.stringify(
  {
    mode: "dark",
    active: "true",
    nums: ["1", "2", "3"],
  },
  { arrayFormat: "repeat" }
);
콘솔
"mode=dark&active=true&nums=1&nums=2&nums=3"

query-string

두 번째로 살펴볼 라이브러리는 qs 못지않은 인지도를 보유하고 있는 query-string인데요.

query-string은 npm을 사용하여 설치할 수 있고요.

$ npm i query-string

패키지를 불러올 때는 하나의 모듈로 불러오면 됩니다.

import queryString from "query-string";

query-stringqs와 마찬가지로 parse() 함수를 제공하는데요. 이 함수를 통해서 문자열 형태의 쿼리 스트링을 객체의 형태로 변환할 수 있습니다.

const obj = qs.parse("mode=dark&active=true&nums=1&nums=2&nums=3");
console.log(obj);
콘솔
{
  active: "true",
  mode: "dark",
  nums: ["1", "2", "3"],
}

그런데 주의 깊게 보시면 결과 객체가 파라미터 이름 기준으로 오름차순 정렬되어 있는 것을 알 수 잇습니다. 만약에 정렬을 하고 싶지 않다면 두 번째 인자로 sort 옵션을 false로 설정하면 됩니다.

const obj = qs.parse("mode=dark&active=true&nums=1&nums=2&nums=3", {
  sort: false,
});
console.log(obj);
콘솔
{
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
}

qs와 마찬가지로 query-stringstringify() 함수를 이용하면 자바스크립트 객체 형태의 쿼리 스트링을 문자열로 변환할 수 있습니다.

const str = queryString.stringify({
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
});
console.log(str);
콘솔
"active=true&mode=dark&nums=1&nums=2&nums=3"

query-stringqs와 다르게 기본적으로 동일한 파라미터 이름을 여러 번 반복하여 배열 값을 문자열로 변환해주는 것을 볼 수 있습니다.

qs vs. query-string

지금까지 보신 바와 같이 qsquery-string는 거의 동일한 API를 제공하고 있어서 많은 경우 서로 대체해서 사용할 수 있는데요 그래도 몇 가지 자잘한 차이점이 있어서 주의가 필요하며, 가급적 이 두 개의 라이브러리를 같은 프로그램에서 혼용해서 쓰지 않는 것이 좋습니다.

우선 qsparse() 함수는 query-stringparse() 함수와 달리 인자로 넘어온 쿼리 스트링 앞에 ?을 자동으로 제거해주지 않습니다.

const obj = qs.parse("?mode=dark&active=true&nums=1&nums=2&nums=3", {
  sort: false,
});
console.log(obj);
콘솔
{
  "?mode": "dark", // ?mode 😧
  active: "true",
  nums: ["1", "2", "3"],
}

이러한 문제는 두 번째 인자로 ignoreQueryPrefix 옵션을 true로 명시하여 해결할 수 있습니다.

const obj = qs.parse("?mode=dark&active=true&nums=1&nums=2&nums=3", {
  ignoreQueryPrefix: true,
});
console.log(obj);
콘솔
{
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
}

더 중요한 차이점은 qsstringify() 함수는 복잡한 형태의 객체를 다룰 수 있지만, query-stringstringify() 함수는 중첩되지 않는 단순한 객체만 다룰 수 있다는 것입니다.

const str = qs.stringify({
  foo: {
    bar: "baz",
  },
});
console.log(str);
콘솔
"foo%5Bbar%5D=baz" // "foo[bar]=baz"

query-string을 사용하면 다음과 같이 객체값이 [object Object]로 처리되는 것을 볼 수 있습니다.

const str = queryString.stringify({
  foo: {
    bar: "baz",
  },
});
console.log(str);
콘솔
"foo=%5Bobject+Object%5D" // "foo=[object+Object]"

중첩된 객체를 쿼리 스트링으로 변환하는 것은 웹에서 표준화되어 있지 않기 때문에 query-string 라이브러리에서 일부로 이 기능을 지원하지 않고 있다고 얘기하고 있습니다. 저는 이 부분에 대해서는 어느 정도 동의하는 편인데요. 그렇게 복잡한 데이터라면 쿼리 스트링보다는 요청 바디(body)에 JSON 형태로 전송하는 것이 더 나을 것이기 때문입니다.

정리를 해보면 qsquery-string보다 전반적으로 제공하는 기능이 더 많고 자연스럽게 패키지 용량도 더 큽니다. 따라서 복잡한 쿼리 스트링을 다루어야할 때는 qs를 추천드리고, 단순한 쿼리 스트링만 다룰 때는 query-string을 추천드립니다.

라이브러리를 사용할 수 없다면?

만약에 외부 라이브러리를 사용할 수 없는 환경에서 개발하는 경우라면 어떻게 해야할까요?

사실 이러한 라이브러리에서 제공하는 함수는 웹 표준 API인 URLSearchParams를 사용하면 크게 어렵지 않게 직접 구현할 수 있는데요. 왜냐하면 URLSearchParams&를 기준으로 문자열를 분할하거나 인코딩/디코딩같은 까다로운 작업을 내부적으로 처리해주기 때문입니다.

자바스크립트의 URLSearchParams에 대한 자세한 설명은 관련 포스팅을 참고 바랍니다.

예를 들어, 문자열 형태의 쿼리 스트링을 객체 형태로 바꿔주는 parse() 함수를 작성해보겠습니다.

function parse(queryString) {
  var searchParams = new URLSearchParams(queryString);
  var result = {};
  for (const [key, value] of searchParams) {
    // 파라미터가 이미 존재한다면 값을 배열로 바꾼 후에 값을 추가
    if (key in result) {
      if (!Array.isArray(result[key])) {
        result[key] = [result[key]];
      }
      result[key].push(value);
    }
    // 새로운 파라마터는 그냥 키에 값을 할당
    else {
      result[key] = value;
    }
  }
  return result;
}

한번 테스트해볼까요?

const obj = parse("?mode=dark&active=true&nums=1&nums=2&nums=3");
console.log(obj);
콘솔
{
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
}

이번에는 반대로 객체 형태의 쿼리 스트링을 문자열 형태로 바꿔주는 stringify() 함수를 작성해보겠습니다.

function stringify(queryObject) {
  var searchParams = new URLSearchParams();
  Object.entries(queryObject).forEach(([key, value]) => {
    // 값이 배열이라면 동일한 파라미터 키에 여러 값을 추가
    if (Array.isArray(value)) {
      value.forEach((item) => searchParams.append(key, item));
    }
    // 값이 배열이 아니라면 하나의 파라미터 키에 하나의 값을 추가
    else {
      searchParams.append(key, value);
    }
  });

  return searchParams.toString();
}

역시 한번 테스트해볼까요?

const str = stringify({
  mode: "dark",
  active: "true",
  nums: ["1", "2", "3"],
});
console.log(str);
콘솔
"mode=dark&active=true&nums=1&nums=2&nums=3"

어떤가요? 직접 구현해보니 생각했던 것보다 어렵지 않죠?

마치면서

지금까지 자바스크립트에서 쿼리 스트링을 다루기 위해서 사용되는 대표적인 라이브러리인 qsquery-string에 대해서 살펴보았습니다. 또한 URLSearchParams을 활용하여 이러한 라이브러리가 제공하는 기능을 직접 구현도 해보았습니다.