[React JS] 특정 브라우저에서만 URLSearchParams 의 size 가 null 일 때

2025. 5. 18. 12:26React JS

인생을 살다보면 URLSearchParams 을 활용해 쿼리스트링값을 활용해야할 때가 있다.

더 나아가, 쿼리스트링 오브젝트의 크기를 알고 싶을 때가 있다.

 

이때 자바스크립트는 좋은 api를 제공한다. 바로 URLSearchParams().size 이다.

문제

하지만 특정 브라우저에서는(내 경우에는 IPad OS 16의 Safari 16.5) URLSearchParams().size 를 찍으면 자꾸 null로 떴다. can i use를 들어가보면 URLSearchParams는 대부분의 브라우저에서 지원하고 있다. 도대체 뭐가 문제인걸까.

https://caniuse.com/?search=URLSearchParams

원인

can i use를 들여다보니 size 는 지원하는 범위가 남달랐다. 진짜 최신 브라우저는 지원하지만 2년 정도 전 버전의 브라우저에서는 지원하지 않는다고 되어있다! 나는 운좋게도 지원하지 않는 브라우저에 걸쳐있어서 이 현상을 발견할 수 있었다.

https://caniuse.com/mdn-api_urlsearchparams_size

해결방안

다행히 현재 React 프로젝트에서는 하나의 공통 모듈에서 URLSearchParams를 사용하여 쿼리스트링을 조회하거나 추가 및 변경하는 커스텀훅 및 함수를 제공하고 있었다. 그래서 URLSearchParams 를 상속한 class 를 만들고 이를 사용하도록 하였다.

size 멤버함수를 override 함으로서 현재 개발 소스는 변경하지 않고 URLSearchParams().size를 지원하지 않는 브라우저에서도 size를 사용할 수 있도록 하였다.

 

URLSearchParams 은 forEach라는 함수를 제공하는 데 현재 쿼리스트링을 순회한다. 이를 활용하여 size를 구현할 수 있다.

구현코드에서 CustomURLSearchParams를 참고하면 된다.

구현코드

import { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { getHistoryRouterState, globalHistory } from "./browserHistoryHelpers";

type QueryString =
  | URLSearchParams
  | string
  | Record<string, string>
  | string[][];

export class CustomURLSearchParams extends URLSearchParams {
  constructor(
    init?: string[][] | Record<string, string> | string | URLSearchParams
  ) {
    super(init);
  }

  //NOTE- size가 크로스브라우징이 안 좋음; https://caniuse.com/mdn-api_urlsearchparams_size
  // 그래서 그거 오버라이딩 한 것
  override get size() {
    let count = 0;

    this.forEach(() => count++);

    return count;
  }
}

export const getQueryStringValues = (
  queryString: QueryString = location.search
) => {
  const searchParams = new CustomURLSearchParams(queryString);
  return searchParams;
};

export const createQueryString = (
  params: Record<string, any>,
  prevParams?: Record<string, any>
) => {
  const searchParams = new CustomURLSearchParams(prevParams);
  Object.entries(params).forEach(([key, value]) => {
    if (value === undefined || value === null || value === "") {
      searchParams.delete(key);
    } else {
      searchParams.set(key, String(value));
    }
  });
  return searchParams;
};

export const useQueryString = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  // const { state } = useLocation(); // 이렇게 구독할 필요가 있을까. history.state.usr로 똑같이 조회 가능한데;
  const params = useMemo(
    () => getQueryStringValues(searchParams.toString()),
    [searchParams]
  );

  const getParams = useCallback(() => params, [params]);

  const setParams = useCallback((newParams: Record<string, any>) => {
    setSearchParams(
      (prevParams) => {
        const queryString = createQueryString(newParams, prevParams);
        return queryString;
      },
      { replace: true, state: getHistoryRouterState() ?? null }
    );
  }, []);

  /**
   * 이 함수는 렌더링을 유발하지 않음.
   * 필요할 때만 사용
   */
  const simplySetParams = useCallback(
    (newParams: Record<string, any>) => {
      const prevParams = Object.fromEntries(searchParams);
      const queryString = createQueryString(newParams, prevParams);

      // History API를 사용하여 URL 업데이트
      window.history.replaceState(
        globalHistory.state ?? null,
        "",
        `${window.location.pathname}?${queryString.toString()}`
      );
    },
    [searchParams]
  );

  return {
    params,
    getParams,
    setParams,
    simplySetParams,
  };
};

 

느낀 점

지금은 다행히 URLSearchParams를 감싸서 한 곳에서 사용해서 변경에 나름 용이했지만,
사실 제일 좋은 것은 프로젝트 내에서 공통적으로 URLSearchParams를 확장하는 것이라고 생각한다. 분명 어떤 개발 소스에서는 공통 모듈의 존재를 망각하고 날것의 URLSearchParams를 사용할 수도 있기 때문이다.

이것에 대해 알아두면 정말 좋을 것 같다!

참고