91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
"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<T extends Record<string, ParamValue>>(
|
|
defaults: T,
|
|
): UseSearchParamsStateReturn<T> {
|
|
const searchParams = useSearchParams();
|
|
const defaultsRef = useRef(defaults);
|
|
|
|
const params = useMemo(
|
|
() => parseParams(searchParams, defaultsRef.current),
|
|
[searchParams],
|
|
);
|
|
|
|
const setParams = useCallback((partial: Partial<T>) => {
|
|
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<T extends Record<string, ParamValue>>(
|
|
searchParams: ReturnType<typeof useSearchParams>,
|
|
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<T> {
|
|
params: T;
|
|
setParams: (partial: Partial<T>) => void;
|
|
resetParams: () => void;
|
|
}
|