검색 기능은 클라이언트와 서버에 걸쳐 작동합니다. 사용자가 클라이언트에서 송장을 검색하면 URL 매개변수가 업데이트되고 서버에서 데이터를 가져와 테이블이 새 데이터로 서버에서 다시 렌더링됩니다.
왜 URL 검색 매개변수를 사용할까요?
위에서 언급한대로 검색 상태를 관리하기 위해 URL 검색 매개변수를 사용할 것입니다. 이 패턴은 클라이언트 측 상태로 작업하는 데 익숙하다면 새로울 수 있습니다.
URL 매개변수를 사용하는 검색의 몇 가지 이점은 다음과 같습니다:
북마크 및 공유 가능한 URL: 검색 매개변수가 URL에 포함되어 있기 때문에 사용자는 애플리케이션의 현재 상태를 북마크하여 나중에 참조하거나 공유할 수 있습니다.
서버 측 렌더링 및 초기 로드: URL 매개변수는 초기 상태를 렌더링하는 데 직접적으로 사용될 수 있어 서버 렌더링을 처리하기 쉽습니다.
분석 및 추적: URL에 검색 쿼리와 필터가 직접 포함되어 있기 때문에 추가적인 클라이언트 측 로직 없이 사용자 행동을 추적하기가 더 쉽습니다.
검색 기능 추가하기
다음은 검색 기능을 구현하는 데 사용할 Next.js 클라이언트 훅들입니다:
useSearchParams - 현재 URL의 매개변수에 액세스할 수 있게 합니다. 예를 들어 이 URL /dashboard/invoices?page=1&query=pending의 검색 매개변수는 다음과 같이 보일 것입니다: {page: '1', query: 'pending'}.
usePathname - 현재 URL의 경로명을 읽을 수 있게 합니다. 예를 들어 /dashboard/invoices 경로의 경우, usePathname은 '/dashboard/invoices'를 반환합니다.
구현 단계에 대한 간단한 개요입니다:
사용자 입력을 캡처합니다.
검색 매개변수로 URL을 업데이트합니다.
URL을 입력 필드와 동기화합니다.
검색 쿼리를 반영하여 테이블을 업데이트합니다.
1. 사용자 입력 캡처하기
<Search> 컴포넌트 (/app/ui/search.tsx)로 이동하면 다음을 볼 수 있습니다:
"use client" - 이는 클라이언트 컴포넌트로, 이벤트 리스너와 훅을 사용할 수 있음을 의미합니다.
<input> - 이는 검색 입력란입니다.
handleSearch 함수를 만들고, <input> 요소에 onChange 리스너를 추가하세요. onChange는 입력 값이 변경될 때마다 handleSearch를 호출할 것입니다.
이 함수는 handleSearch 내용을 감싸고 사용자가 타이핑을 멈춘 후에만 코드를 실행합니다(300ms 후).
다시 검색 바에 입력하고 개발 도구 콘솔을 확인해보세요. 다음과 같은 내용이 표시됩니다:
개발 도구 콘솔
검색 중... Emil
디바운싱을 통해 데이터베이스로 전송되는 요청 수를 줄일 수 있어서 리소스를 절약할 수 있습니다.
퀴즈 시간입니다!
지금까지 배운 내용을 테스트해보세요.
검색 기능에서 디바운싱이 해결하는 문제는 무엇인가요?
A: 데이터베이스 쿼리 속도를 높입니다.
B: URL 북마크 기능을 추가합니다.
C: 매 입력마다 데이터베이스 쿼리를 방지합니다.
D: SEO 최적화에 도움이 됩니다.
정답 확인
C: 매 입력마다 데이터베이스 쿼리를 방지합니다.
맞았습니다! 디바운싱은 키를 누를 때마다 새로운 데이터베이스 쿼리를 방지하여 리소스를 절약합니다.
페이지네이션 추가
검색 기능을 도입한 후에는 테이블이 한 번에 6개의 송장만 표시되는 것을 알 수 있습니다. 이는 data.ts의 fetchFilteredInvoices() 함수가 한 페이지당 최대 6개의 송장을 반환하기 때문입니다.
페이지네이션을 추가하면 사용자가 다른 페이지를 탐색하여 모든 송장을 볼 수 있습니다. 검색과 마찬가지로 URL 매개변수를 사용하여 페이지네이션을 구현하는 방법을 살펴보겠습니다.
<Pagination/> 컴포넌트로 이동하면 클라이언트 컴포넌트임을 알 수 있습니다. 데이터베이스 비밀 키를 노출시킬 수 있기 때문에 데이터를 클라이언트에서 가져오지 않아야 합니다.(기억하세요, API 레이어를 사용하지 않고 있습니다). 대신, 서버에서 데이터를 가져와 컴포넌트에 prop으로 전달할 수 있습니다.
/dashboard/invoices/page.tsx에서 fetchInvoicesPages라는 새 함수를 가져와 searchParams에서 query를 인수로 전달하세요.
<Pagination/> 컴포넌트로 이동하고 usePathname 및 useSearchParams 훅을 가져옵니다. 현재 페이지를 가져와 새 페이지를 설정하는 데 사용할 것입니다. 또한 이 컴포넌트의 코드를 주석 해제해야 합니다. 아직 <Pagination/> 로직을 구현하지 않았기 때문에 일시적으로 애플리케이션이 중단될 수 있습니다. 이제 그 부분을 해결해 봅시다!
/app/ui/invoices/pagination.tsx
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
// ...
}
그 다음, <Pagination> 컴포넌트 내부에 createPageURL이라는 새 함수를 만듭니다. 검색과 유사하게 URLSearchParams를 사용하여 새 페이지 번호를 설정하고 pathName을 사용하여 URL 문자열을 생성할 것입니다.
/app/ui/invoices/pagination.tsx
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
// ...
}
여기에서 무슨 일이 일어나는지 살펴보겠습니다:
createPageURL은 현재 검색 매개변수의 인스턴스를 만듭니다.
그런 다음 "page" 매개변수를 제공된 페이지 번호로 업데이트합니다.
마지막으로 경로 이름과 업데이트된 검색 매개변수를 사용하여 전체 URL을 생성합니다.
나머지 <Pagination> 컴포넌트는 스타일링과 다양한 상태 (첫 번째, 마지막, 활성, 비활성 등)를 다룹니다. 이 강의에서는 자세히 다루지 않겠지만, 코드를 살펴보고 createPageURL이 어디에서 호출되는지 확인해보세요.
마지막으로 사용자가 새로운 검색 쿼리를 입력할 때 페이지 번호를 1로 재설정하고 싶습니다. <Search> 컴포넌트의 handleSearch 함수를 업데이트하여 이를 수행할 수 있습니다.