Next.js
React 필터 상태를 URL 쿼리스트링으로 관리하는 이유
페이지네이션이나 필터 조건을 컴포넌트 state로만 관리하면 URL을 공유했을 때 상태가 날아간다. URL 쿼리스트링으로 옮기면 해결되는 문제들이 있다.
목록 페이지에 필터와 페이지네이션을 추가하면서 처음에는 useState로 상태를 관리했다. 잘 동작하다가 테스트 중에 문제를 발견했다. 필터를 적용한 상태에서 URL을 복사해 다른 탭에 붙여넣으면 필터가 초기화되어 있었다. 공유도 안 되고, 새로고침하면 상태가 날아간다.
언제 URL로 옮기면 좋을까
모든 상태를 URL에 넣을 필요는 없다. 다음 조건에 해당하면 URL 쿼리스트링으로 관리하는 게 낫다.
- 사용자가 특정 상태의 URL을 공유하거나 북마크할 수 있어야 할 때
- 새로고침해도 상태가 유지되어야 할 때
- 뒤로가기/앞으로가기가 필터 상태 변경에 동작해야 할 때
검색어, 정렬 기준, 페이지 번호, 카테고리 필터 같은 것들이 여기 해당한다.
Next.js에서 구현하는 방법
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
function FilterBar() {
const router = useRouter();
const searchParams = useSearchParams();
const category = searchParams.get('category') ?? 'all';
const page = Number(searchParams.get('page') ?? '1');
function setFilter(key: string, value: string) {
const params = new URLSearchParams(searchParams.toString());
params.set(key, value);
if (key !== 'page') params.set('page', '1'); // 필터 바꾸면 1페이지로 리셋
router.push(`?${params.toString()}`);
}
return (
<select value={category} onChange={e => setFilter('category', e.target.value)}>
<option value="all">전체</option>
<option value="dev">개발</option>
<option value="ops">운영</option>
</select>
);
}
useSearchParams로 현재 쿼리를 읽고, 변경 시 router.push로 URL을 업데이트한다. 기존 파라미터를 URLSearchParams로 복사한 뒤 원하는 키만 바꾸는 방식이 깔끔하다.
주의할 점
필터를 바꿀 때 페이지 번호를 1로 리셋하지 않으면, 카테고리를 바꿔도 3페이지가 유지되어 결과가 없는 것처럼 보이는 경우가 생긴다.
SSR 환경에서는 searchParams를 서버 컴포넌트의 props로 받아서 서버에서 바로 필터링할 수도 있다. 클라이언트에서 추가 fetch를 날리지 않아도 되니 초기 로딩이 빠르다.
// app/posts/page.tsx (서버 컴포넌트)
export default async function PostsPage({
searchParams,
}: {
searchParams: { category?: string; page?: string };
}) {
const category = searchParams.category ?? 'all';
const page = Number(searchParams.page ?? '1');
const posts = await getPosts({ category, page });
return <PostList posts={posts} />;
}
상태를 URL에 두면 공유·북마크·뒤로가기가 자동으로 해결된다. 처음에 조금 더 코드가 늘어나지만, 나중에 생기는 UX 문제를 처음부터 막는다.