문제 상황


하나의 커스텀 훅으로 여러 페이지에서 쿼리스트링을 통한 데이터 요청을 구현하였다. 해당 커스텀 훅은 useSearchParams 를 사용하여 get, set 메서드를 반환한다. 아래는 최초로 작성했던 코드이다.

import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useCallback } from "react";

/**
 * @example
 * const value = get(<keyA>);
 * set({ <keyA>: <valueA>, <keyB>: <valueB> });
 * set({ <keyA>: null });
 * set({ <keyA>: null, <keyB>: <valueB> });
 */
export function useQueryParams() {
	const searchParams = useSearchParams();
	const router = useRouter();

	const get = (key: string) => searchParams.get(key);

	const set = useCallback(
		(params: Record<string, string | null | undefined>) => {
			const newParams = new URLSearchParams(searchParams.toString());

			Object.entries(params).forEach(([key, value]) => {
				if (value === null || value === undefined) {
					newParams.delete(key);
				} else {
					newParams.set(key, value);
				}
			});

			router.replace(`?${newParams.toString()}`);
		},
		[searchParams, router],
	);

	return { get, set };
}

그러나 커스텀 훅에서 router.replace 를 사용하는 경우 렌더링 속도가 심각하게 저하되는 문제가 발생했다. 이를 해결하기 위해 Next.js 공식문서를 확인하고 문제가 없다는 판단 하에 window.history.replaceState 로 변경하였다.

const router = useRouter();
// ...
router.replace(`?${newParams.toString()}`);

// 아래와 같이 변경
window.history.replaceState(null, "", `?${newParams.toString()}`);

그러나 코드를 바꾸고 나서 2가지 이슈가 발생하였다.

  1. 쿼리스트링을 가져올 때 팀원 A는 커스텀 훅의 get (useSearchParams ), 팀원 B는 prefetch 를 위해 서버 컴포넌트에서 searchParams prop 을 사용했다. 팀원 A의 페이지에서는 문제가 없었지만 팀원 B의 페이지에서 브라우저 주소만 변경되고 리렌더링되지 않았다.
  2. 간헐적으로 쿼리스트링과 API 요청 시 Request 값이 일치하지 않는 경우가 생겼다.

해결법 및 해결 과정


  1. set 의 의존성에서 router 제거 → []로 변경
  2. searchParams.toString()window.location.search로 변경

최종 코드는 아래와 같다.

import { useSearchParams } from "next/navigation";
import { useCallback } from "react";

/**
 * @example
 * const value = get(<keyA>);
 * set({ <keyA>: <valueA>, <keyB>: <valueB> });
 * set({ <keyA>: null });
 * set({ <keyA>: null, <keyB>: <valueB> });
 */
export function useQueryParams() {
	const searchParams = useSearchParams();

	const get = (key: string) => searchParams.get(key);

	const set = useCallback((params: Record<string, string | null | undefined>) => {
		const newParams = new URLSearchParams(window.location.search);

		Object.entries(params).forEach(([key, value]) => {
			if (value === null || value === undefined) {
				newParams.delete(key);
			} else {
				newParams.set(key, value);
			}
		});
		
		window.history.replaceState(null, "", `?${newParams.toString()}`);
	}, []);

	return { get, set };
}
  1. 팀원 B의 페이지에서 searchParams 는 prefetch 할 때만 사용하도록 기존 부모→자식 컴포넌트 전달을 제거하고, useSearchParams 를 자식에서 사용하도록 구조를 변경하였다.