useSearchParams : 리엑트 query string 관리

개발할 때 링크에 복잡한 쿼리스트링 파라미터를 쉽게 관리하는 패턴을 커스텀훅으로 만들었습니다. 기존에 서버사이드 개발 시 요긴하게 사용했던 함수의 방식을 리엑트로 옮긴 것입니다. 링크 URL을 만들 때 그 기능의 맥락만 생각하면 되기 때문에 파라미터 복잡도가 높아졌을 때 유용합니다.

간단히 커스텀훅 코드와 사용 예를 적어두는 기록용으로 올립니다.

사용 예

아래처럼 withSearchParams 함수를 꺼내서 사용합니다.
searchParams는 쿼리스트링 Object입니다.

const { searchParams, withSearchParams } = useSearchParams();

withSearchParams는 인자로 url 와 options 를 넘겨 실행합니다.
옵션없이 적용하면 현재 쿼리스트링을 그대로 적용해줍니다.

// 현재 URI : /abc?page=2&searchKeyword=foo
<Link to={withSearchParams(`/articles/${id}/detail`)}>
  {subject}
</Link>

// 결과
<Link to={`/articles/${id}/detail?page=2&searchKeyword=foo`}>
  {subject}
</Link>

새로 파라미터를 설정할 때는 set 옵션을 사용합니다.
기존에 존재하는 키는 값만 변경합니다.

// 현재 URI : /articles?foo=1&bar=2
withSearchParams(`/articles`, { set: { page: 1 } });
// 페이지 설정(추가) : /articles?foo=1&bar2&page=1

// 현재 URI : /articles?foo=1&bar=2&page=1
withSearchParams(`/articles`, { set: { page: 2 } });
// 페이지 설정(변경) : /articles?&foo=1&bar2&page=2

일부 파라미터를 제거할 때는 remove 옵션을 사용합니다.
제거할 키값을 배열로 나열합니다.

// 현재 URI : /articles?foo=1&bar=2&page=2
withSearchParams('/articles', { remove: ['foo','bar'] });
// 결과 : /articles?page=2

set, remove를 함께 사용할 경우도 있습니다.

// 현재 URI : /articles?page=4&searchKeyword=foo
withSearchParams('/articles', { set: { filter: 'a' }, remove: ['page'] });
// 결과 : /articles?searchKeyword=foo&filter=a

모든 파라미터를 제거 하고 새로 설정할 경우입니다.

// 현재 URI : /articles?searchKeyword=foo&filter=a
withSearchParams('/some', { set: { foo: 1 }, skipAll: true });
// 결과 : /some?foo=1

uri 문자열에 이미 쿼리스트링이 포함되어 있는 경우도 처리합니다.

// 현재 URI : /articles?baz=3&page=1
withSearchParams('/some?foo=1&bar=2', { set: { page: 5 }, remove: ['foo'] });
// 결과 : /some?baz=3&page=5&bar=2

useSearchParams.js

import { useState, useCallback, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';

function getSearchParams(parsedSearchParams = {}, options = {}) {
  const params = Object.keys(options).reduce((params, optionType) => {
    if (optionType === 'set') {
      if (options.skipAll) {
        return options.set;
      }
      return { ...params, ...options.set };
    }

    if (optionType === 'remove') {
      return Object.keys(params).reduce((obj, key) => {
        if (options.remove.includes(key) === false) {
          obj[key] = params[key];
        }
        return obj;
      }, {});
    }

    return params;
  }, parsedSearchParams);

  return queryString.stringify(params, {
    skipEmptyString: options?.skipEmpty,
    skipNull: options?.skipEmpty,
  });
}

function useSearchParams() {
  const { search } = useLocation();
  const [searchParams, setSearchParams] = useState(queryString.parse(search));

  useEffect(() => {
    setSearchParams(queryString.parse(search));
  }, [search]);

  const withSearchParams = useCallback(
    (uri, options) => {
      const { url, query, fragmentIdentifier } = queryString.parseUrl(uri, {
        parseFragmentIdentifier: true,
      });

      const newQuery = getSearchParams({ ...searchParams, ...query }, options);

      return `${url}${newQuery ? `?${newQuery}` : ''}${
        fragmentIdentifier ? `#${fragmentIdentifier}` : ''
      }`;
    },
    [searchParams],
  );

  return {
    searchParams,
    setSearchParams,
    getSearchParams,
    withSearchParams,
  };
}

export default useSearchParams;