
현대 웹 애플리케이션에서는 사용자 입력에 따라 다양한 필터링 옵션을 제공하고, 이를 기반으로 API에서 데이터를 가져오는 일이 빈번합니다. 이때 필터 상태를 효과적으로 관리하고, URL과 동기화하여 사용자 경험을 향상시키는 방법 중 하나가 바로 URL 검색 매개변수(search parameters)를 사용하는 것입니다.
이번 포스트에서는 useQueryParamsState라는 커스텀 훅을 만들어, 여러 필터 데이터를 URL 검색 매개변수로 관리하고 API에 전달하는 방법을 소개하겠습니다.
useQueryParamsState 커스텀 훅 만들기먼저, URL 검색 매개변수와 상태를 동기화하는 커스텀 훅을 만들어보겠습니다.
import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { useLocation } from "react-router-dom";
type UseQueryParamsStateReturnType<T> = [T, Dispatch<SetStateAction<T>>];
interface UseQueryParamsStateOptions {
shouldClearParamsOnRefresh?: boolean;
}
export const useQueryParamsState = <T extends object>(
initialState: T,
options?: UseQueryParamsStateOptions
): UseQueryParamsStateReturnType<T> => {
const location = useLocation();
// 객체 상태로 여러 쿼리 파라미터를 관리
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initialState;
const { search } = window.location;
const searchParams = new URLSearchParams(search);
const paramsObject: Partial<T> = {};
// URL에서 쿼리 파라미터를 읽어서 객체로 변환
searchParams.forEach((paramValue, paramKey) => {
paramsObject[paramKey as keyof T] = paramValue as T[keyof T];
});
return { ...initialState, ...paramsObject } as T;
});
// 쿼리 파라미터를 URL에 반영하는 효과
useEffect(() => {
const currentSearchParams = new URLSearchParams(window.location.search);
Object.keys(value).forEach((key) => {
const paramValue = value[key as keyof T] as T[keyof T];
if (paramValue !== null && paramValue !== "") {
currentSearchParams.set(key, String(paramValue));
} else {
currentSearchParams.delete(key);
}
});
// 새로운 URL 조합
const newUrl = [window.location.pathname, currentSearchParams.toString()]
.filter(Boolean)
.join("?");
window.history.replaceState(window.history.state, "", newUrl);
}, [value, location.pathname]);
// 새로고침 시 쿼리 파라미터 제거 옵션 처리
useEffect(() => {
const shouldClearParams = options?.shouldClearParamsOnRefresh ?? false;
// 컴포넌트가 처음 마운트될 때만 쿼리 파라미터 제거
if (shouldClearParams) {
const clearSearchParams = () => {
setValue(initialState); // 초기 상태로 되돌림
const newUrl = window.location.pathname;
window.history.replaceState(null, "", newUrl);
};
// 새로고침 등 첫 마운트 시에만 쿼리 파라미터 제거
clearSearchParams();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options?.shouldClearParamsOnRefresh]);
return [value, setValue];
};
useState 훅을 사용하여 URL의 검색 매개변수를 객체 형태로 상태에 저장합니다. 이때 initialState와 URL에서 가져온 매개변수를 병합하여 초기 상태를 설정합니다.useEffect 훅을 통해 상태(value)가 변경될 때마다 URL의 검색 매개변수를 업데이트합니다. 이로써 상태와 URL이 항상 동기화됩니다.shouldClearParamsOnRefresh 옵션을 통해 컴포넌트가 처음 마운트될 때 검색 매개변수를 제거할지 여부를 결정합니다.이제 이 커스텀 훅을 사용하여 사용자 입력에 따라 필터링된 데이터를 API에서 가져오는 컴포넌트를 만들어보겠습니다.
import { useQuery } from "@tanstack/react-query";
import { api } from "./api";
import { useQueryParamsState } from "./useQueryParamsState";
import { AxiosResponse } from "axios";
import { debounce } from "lodash";
import { useMemo } from "react";
const Comp1 = () => {
const [searchParams, setSearchParams] = useQueryParamsState(
{
test1: "",
test2: "",
isSearching: false,
},
{
shouldClearParamsOnRefresh: false,
}
);
const enabled =
typeof searchParams.isSearching === "string"
? searchParams.isSearching === "false"
? true
: false
: !searchParams.isSearching;
const a = useQuery<
AxiosResponse<{ results: { name: string; id: string }[] }>
>({
queryKey: ["GET_MOVIE", searchParams],
queryFn: async () => {
const response = await api.get(
`/3/search/keyword?query=${searchParams.test1}&page=1`
);
return response;
},
enabled,
});
const handleSearchingStatus = useMemo(
() =>
debounce(
() =>
setSearchParams((prev) => {
return {
...prev,
isSearching: false,
};
}),
3000
),
[setSearchParams]
);
return (
<div>
<input
name="test1"
value={searchParams.test1}
onChange={(e) => {
handleSearchingStatus();
setSearchParams((prev) => {
return {
...prev,
isSearching: true,
[e.target.name]: e.target.value,
};
});
}}
/>
<input
name="test2"
value={searchParams.test2}
onChange={(e) => {
setSearchParams((prev) => {
return {
...prev,
isSearching: true,
[e.target.name]: e.target.value,
};
});
}}
/>
{a.data?.data.results.map((el) => {
return <div key={el.id}>{el.name}</div>;
})}
</div>
);
};
export default Comp1;
useQueryParamsState 훅을 사용하여 test1, test2, isSearching 세 가지 필터 상태를 관리합니다.enabled 변수를 통해 isSearching 상태에 따라 쿼리 실행 여부를 결정합니다. 검색 중일 때는 쿼리를 비활성화하여 불필요한 API 호출을 방지합니다.useQuery 훅을 사용하여 검색어에 따라 API에서 데이터를 가져옵니다. queryKey에 searchParams를 포함하여 검색어가 변경될 때마다 새로운 데이터를 가져오도록 합니다.handleSearchingStatus 함수를 useMemo와 debounce를 사용하여 생성합니다. 사용자가 입력을 멈추고 3초 후에 isSearching 상태를 false로 변경하여 쿼리를 활성화합니다.test1)에서는 사용자가 입력할 때마다 isSearching을 true로 설정하고, 디바운싱된 함수로 isSearching을 false로 변경합니다.test2)에서는 isSearching을 true로 설정하지만 디바운싱 없이 바로 상태를 업데이트합니다.isSearching 상태와 enabled 옵션을 사용하여 불필요한 API 호출을 방지하고, 사용자가 입력을 마친 후에만 데이터를 가져옵니다.useQueryParamsState 훅에서 제네릭 타입 T를 object로 지정하여 더 다양한 형태의 상태를 지원할 수 있도록 했습니다.null이거나 빈 문자열인 경우 URL에서 해당 매개변수를 제거하여 URL을 깨끗하게 유지합니다.URL 검색 매개변수를 활용하여 여러 필터 데이터를 관리하면 사용자 경험을 향상시키고 애플리케이션의 상태 관리도 용이해집니다. 특히, API 호출을 효율적으로 관리하고 상태와 URL을 동기화하여 사용자에게 일관된 경험을 제공할 수 있습니다.
useQueryParamsState와 같은 커스텀 훅을 사용하면 이러한 기능을 손쉽게 구현할 수 있으며, 코드의 재사용성과 유지 보수성도 높일 수 있습니다. 앞으로 복잡한 필터링 기능을 구현하거나 사용자 상태를 URL과 동기화해야 하는 경우 이와 같은 방법을 고려해보시기 바랍니다.
참고: 이 글의 예제 코드는 이해를 돕기 위한 간단한 버전입니다. 실제 애플리케이션에서는 에러 처리, 로딩 상태 관리, 입력 값 유효성 검사 등을 추가로 구현해야 합니다.