"use client"; import { useCallback, useMemo, useRef } from "react"; import { useSearchParams } from "next/navigation"; /** * 조회 조건을 URL searchParams와 동기화하는 훅. * * - URL이 단일 소스 오브 트루스(SSOT)이며, 새로고침·뒤로가기 시에도 조건이 유지된다. * - 기본값과 동일한 값은 URL에서 제거해 쿼리스트링을 깨끗하게 유지한다. * - `window.history.replaceState` 기반 shallow 갱신으로 서버 라운드트립 없이 동작한다. * - 숫자 기본값이 주어진 키는 자동으로 `Number` 파싱한다. * * @example * const { params, setParams, resetParams } = useSearchParamsState({ * keyword: "", * page: 0, * size: 10, * }); */ export function useSearchParamsState>( defaults: T, ): UseSearchParamsStateReturn { const searchParams = useSearchParams(); const defaultsRef = useRef(defaults); const params = useMemo( () => parseParams(searchParams, defaultsRef.current), [searchParams], ); const setParams = useCallback((partial: Partial) => { const next = new URLSearchParams(window.location.search); for (const [key, value] of Object.entries(partial)) { if (value === undefined || value === defaultsRef.current[key]) { next.delete(key); } else { next.set(key, String(value)); } } replaceUrl(next); }, []); const resetParams = useCallback(() => { const next = new URLSearchParams(window.location.search); for (const key of Object.keys(defaultsRef.current)) { next.delete(key); } replaceUrl(next); }, []); return { params, setParams, resetParams }; } function parseParams>( searchParams: ReturnType, defaults: T, ): T { const result = { ...defaults }; for (const key of Object.keys(defaults) as (keyof T)[]) { const raw = searchParams?.get(key as string) ?? null; if (raw === null) continue; if (typeof defaults[key] === "number") { const parsed = Number(raw); if (!Number.isNaN(parsed)) result[key] = parsed as T[keyof T]; } else { result[key] = raw as T[keyof T]; } } return result; } function replaceUrl(searchParams: URLSearchParams) { const qs = searchParams.toString(); const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname; window.history.replaceState(null, "", url); } type ParamValue = string | number | null; interface UseSearchParamsStateReturn { params: T; setParams: (partial: Partial) => void; resetParams: () => void; }