출처: https://aurorascharff.no/posts/managing-advanced-search-param-filtering-next-app-router/
Next.js 앱에서 고급 필터링 기능을 원한다고 가정해 보겠습니다. 예를 들어 작업 목록이 있고 카테고리와 이름으로 필터링하고 싶을 수 있습니다. 페이지네이션, 정렬 및 기타 기능도 원할 수 있습니다.
이 상태를 URL에 넣으라는 요청은 흔한데, 앱의 현재 상태를 공유하고, 북마크하고, 다시 로드할 수 있기 때문입니다. 하지만, useEffect를 사용하여 URL의 상태를 컴포넌트 상태와 조정하는 것은 어려울 수 있습니다. 대신, URL을 단일 소스로 사용하는 것이 더 좋습니다. 기본적으로 상태를 올리는 것입니다. 이는 React에서 잘 알려진 패턴입니다.
하지만 Next.js 앱 라우터에서 React Server Components 및 기타 새로운 기능과 패턴을 사용할 때 이 상태를 원활하게 관리하기 어려울 수 있습니다. 이 블로그 게시물에서는 Next.js 앱 라우터에서 고급 검색 매개변수 필터링을 구현하는 방법을 살펴보고, React 19 기능(예: useTransition, useOptimistic)을 활용하고, 마지막으로 nuqs 라이브러리로 전환합니다.
필터는 사용자에게 즉각적인 피드백을 제공해야 하며, 여러 필터가 적용되는 경우 각 필터가 다른 필터보다 우선해서는 안 됩니다.
우리는 Search 구성 요소를 사용하고 있습니다: input이 변경되면 URL 검색 파라미터를 업데이트하는 코드가 있습니다.
// Search.tsx
'use client';
...
export default function Search() {
const router = useRouter();
const params = useParams();
const searchParams = useSearchParams();
const q = searchParams.get('q') || '';
return (
<form className="relative flex w-full flex-col gap-1 sm:w-fit">
<label className="font-semibold uppercase" htmlFor="search">
Search
</label>
<input
id="search"
onChange={e => {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.set('q', e.target.value);
router.push(`?${newSearchParams.toString()}`);
}}
defaultValue={q}
className="w-full pl-10 sm:w-96"
name="q"
placeholder="Search in task title or description..."
type="search"
/>
<SearchStatus searching={false} />
</form>
);
}
그리고 Category 필터 구성 요소를 사용하고 있습니다: 마찬가지로 카테고리 필터가 토글되면 URL검색 파라미터가 업데이트 됩니다.
// CategoryFilter.tsx
'use client';
...
export default function CategoryFilter({ categoriesPromise }: Props) {
const categoriesMap = use(categoriesPromise);
const searchParams = useSearchParams();
const router = useRouter();
const selectedCategories = searchParams.getAll('category');
return (
<div>
<ToggleGroup
toggleKey="category"
options={Object.values(categoriesMap).map(category => {
return {
label: category.name,
value: category.id.toString(),
};
})}
selectedValues={selectedCategories}
onToggle={newCategories => {
const params = new URLSearchParams(searchParams);
params.delete('category');
newCategories.forEach(category => {
return params.append('category', category);
});
router.push(`?${params.toString()}`);
}}
/>
</div>
);
}
export default function ToggleGroup({
options,
selectedValues,
toggleKey,
onToggle,
}: Props) {
return (
<div className="flex flex-wrap gap-2">
{options.map((option) => {
const isActive = selectedValues.includes(option.value.toString());
return (
<Link
href={`?${toggleKey}=${isActive ? '' : option.value}`}
key={option.value}
className={cn(
'w-fit rounded border px-4 py-2',
isActive && 'bg-blue-500 text-white',
)}
onClick={(e) => {
e.preventDefault();
if (isActive) {
onToggle(
selectedValues.filter((value) => value !== option.value),
);
} else {
onToggle([...selectedValues, option.value]);
}
}}
>
{option.label}
</Link>
);
})}
</div>
);
}
검색 및 필터 상태를 URL로 푸시한 다음 별도의 page.tsx 서버 구성 요소에서 url querystring를 사용하여 데이터베이스에 직접 쿼리를 보내고 결과를 표로 표시합니다. 이는 SPA 관점에서 검색 및 필터 구성 요소에 대한 논리적 구현입니다.
export default async function RootPage({ params, searchParams }: PageProps) {
const searchParamsResolved = await searchParams;
const { category, q } = searchParamsResolved;
const data = await getTasks({
categories: Array.isArray(category) ? category.map(Number) : category ? [Number(category)] : undefined,
q,
});
return (
<div>
<table>
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Description</th>
<th scope="col">Category</th>
<th scope="col">Created Date</th>
<th scope="col" />
</tr>
</thead>
<tbody>
{data.map(task => {
return (
<tr key={task.id}>
<td className="font-medium">{task.title}</td>
<td>{task.description}</td>
<td>{task.category.name}</td>
<td>{new Date(task.createdAt).toLocaleDateString()}</td>
</tr>
);
})}
{data.length === 0 && (
<tr>
<td className="italic" colSpan={5}>
No tasks found
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
그러나 앱이 예상대로 작동하지 않습니다. 몇 가지 문제가 있습니다.
결국 Next.js 라우터가 작동하는 방식에 달려 있습니다. 이 예의 URL에 주의하세요.