(번역/실습) Managing Advanced Search Param Filtering in the Next.js

기운찬곰·2025년 4월 18일
0

출처: 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>
  );
}
  1. URLSearchParams 객체 생성: 현재 URL의 검색 파라미터를 복사하여 새로운 URLSearchParams 객체 생성합니다. 기존의 다른 검색 파라미터들을 유지하면서 수정할 수 있습니다.
  2. newSearchParams.set('q', value): 'q' 파라미터에 입력된 검색어를 설정합니다. 예를 들어, 사용자가 "hello"를 입력하면 URL이 "?q=hello"로 변경되도록 합니다.
  3. router.push(...): 새로운 검색 파라미터로 URL 업데이트. 페이지 새로고침 없이 URL이 변경됩니다. Next.js의 router.push()는 내부적으로 브라우저의 History API를 사용하여 페이지 새로고침 없이 URL을 변경합니다.

그리고 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>
  );
}

그러나 앱이 예상대로 작동하지 않습니다. 몇 가지 문제가 있습니다.

  • 앱이 검색을 즉시 실행하지 않기 때문에(API 요청/응답 시간) 검색에 대한 onChange가 트리거되었는지 알 수 있는 방법이 없습니다.
  • 카테고리를 클릭한 후 토글 버튼이 활성화되려면 시간이 걸립니다.
  • 카테고리 필터링이 예상대로 작동하지 않습니다. 여러 필터를 빠르게 클릭하면 마지막으로 클릭한 카테고리만 적용됩니다.
  • 검색할 때 완료되기 전에 카테고리를 클릭하면 검색이 취소됩니다(그 반대의 경우도 마찬가지입니다).

문제의 이유

결국 Next.js 라우터가 작동하는 방식에 달려 있습니다. 이 예의 URL에 주의하세요.

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글