검색 결과 페이지네이션의 필요성
검색 결과가 많아지면 슈퍼베이스의 쿼리 개수 제한(1,000개)을 초과하거나 성능이 저하될 수 있다.
이를 해결하기 위해 검색 결과를 n개씩 나눠서 가져오는 페이지네이션 기능을 구현했다.
page 쿼리 파라미터 추가
검색 페이지의 URL을 /search?q=hello&page=2
와 같이 변경하여 검색어와 페이지 정보를 포함하도록 설정한다.
검색어로 URL 업데이트하기
import { useRouter, useSearchParams, usePathname } from "next/navigation";
const { replace } = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const query = searchParams.get("q")?.toString(); // URL에서 가져온 검색어
const [searchQuery, setSearchQuery] = useState(""); // 사용자가 입력한 검색어
// 검색 버튼 클릭 시 검색어로 URL 업데이트
const handleSearch = () => {
// 검색어가 없을 경우 종료
if (!searchQuery) {
return;
}
const params = new URLSearchParams(searchParams);
params.set("q", searchQuery);
replace(`${pathname}?${params.toString()}`);
};
- URL 쿼리 파마리터를 조작하기 위한 메서드를 제공하는
URLSearchParams
를 사용하여?q=hello&page=2
같은 파라미터 문자열을 가져온다. - 사용자가 입력한 값에 따라 파라미터 문자열을
set()
한다. usePathname()
훅의replace()
메서드를 이용해서 URL을 사용자의 입력값으로 업데이트한다.pathname
은/search
와 같이 현재 경로를 가리킨다.params.toString()
은 사용자의 새로운 입력값을 반영한 쿼리 파라미터를 URL 친화적인 형식으로 변환한다.
검색어 입력

<input
defaultValue={searchParams.get('q')?.toString()}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="검색어를 입력하세요"
/>
<button onClick={handleSearch}>검색</button>
searchParams
에서 쿼리 값을 읽어 입력 필드가 URL과 동기화되도록 한다.- 검색 버튼을 클릭하면
handleSearch()
가 실행되어 URL이 업데이트된다.
슈퍼베이스에서 검색 결과 페이징 처리
// src/app/api/search/route.ts
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const searchQuery = searchParams.get("q");
// 페이지네이션
const page = Number(searchParams.get("page")) || 1;
const pageSize = 100; // 한 페이지 데이터 개수
const from = (page - 1) * pageSize;
const to = from + pageSize - 1;
// title, author 필드에서 검색어를 포함하는 데이터 조회, 날짜 내림차순 정렬
const { data, count, error } = await supabase
.from("calendar")
.select("*", { count: "exact" }) // 총 개수 가져오기
.or(`title.like.%${searchQuery}%,author.like.%${searchQuery}%`)
.order("date", { ascending: false })
.range(from, to); // OFFSET & LIMIT 적용
return NextResponse.json({ data, total: count }, { status: 200 });
} catch (error) {
// 에러 처리
}
}
- 쿼리 파라미터에서 검색어와 현재 페이지를 가져와서
from
과to
를 계산한다. - 슈퍼베이스 쿼리에
range(from, to)
조건을 추가해서 검색 결과를 페이지 단위로 가져온다.- 이는 슈퍼베이스에서
LIMIT
과OFFSET
역할을 한다.
- 이는 슈퍼베이스에서
or()
조건으로 검색 쿼리를 실행한다.title
과author
컬럼에서LIKE
연산자를 사용해서searchQuery
문자열이 포함되어 있는 데이터를 출력한다.
- 만약 검색 결과의 총 개수도 얻고 싶다면
select()
에{count: "exact"}
옵션을 추가한다.total: count
값을NextResponse.json()
응답에 추가한다.
페이지네이션 UI 구현
const response = await fetch(
`/api/search?q=${searchQuery}&page=${currentPage}`
);
- 검색 결과를 불러올 때마다
search
API를 호출한다.
import Link from "next/link";
import { pageSize } from "@/lib/constants";
import { usePathname, useSearchParams } from "next/navigation";
export default function Pagination({ totalItems }: { totalItems: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get("page")) || 1; // 현재 페이지
const totalPages = Math.ceil(totalItems / pageSize); // 전체 페이지 수
// 페이지네이션 버튼 목록 생성
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5; // 최대 5개의 페이지 버튼 표시
let start = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let end = Math.min(totalPages, start + maxVisiblePages - 1);
if (end - start < maxVisiblePages - 1) {
start = Math.max(1, end - maxVisiblePages + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
};
// 페이지 클릭 시 URL 생성
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
// pageSize보다 작은 경우 페이지네이션 버튼을 표시하지 않음
if (totalItems <= pageSize) {
return null;
}
return (
<div className="flex justify-center mt-5 space-x-2">
{/* 이전 페이지 버튼 */}
{/* pointer-events-none로 클릭 비활성화 */}
<Link
href={createPageURL(currentPage - 1)}
className={`px-2 py-1 rounded-md ${currentPage === 1 ? "text-gray-300 pointer-events-none" : "text-gray-600 hover:bg-gray-100"}`}
aria-disabled={currentPage === 1}
>
<
</Link>
{/* 페이지 번호 버튼 */}
{getPageNumbers().map((page) => (
<Link
key={page}
href={createPageURL(page)}
className={`px-2 py-1 rounded-md ${currentPage === page ? "text-black font-bold" : "text-gray-600 hover:bg-gray-100"}`}
>
{page}
</Link>
))}
{/* 다음 페이지 버튼 */}
<Link
href={createPageURL(currentPage + 1)}
className={`px-2 py-1 rounded-md ${currentPage === totalPages ? "text-gray-300 pointer-events-none" : "text-gray-600 hover:bg-gray-100"}`}
aria-disabled={currentPage === totalPages}
>
>
</Link>
</div>
);
}
- 이동하고자 하는 페이지의 번호를 클릭하면 새로운 URL을 생성해서
Link
컴포넌트를 통해 페이지를 이동한다.Link
를 사용하면 Next.js의 클라이언트 사이드 네비게이션을 활용할 수 있다.
- 첫 페이지와 마지막 페이지의
Link
기능을 비활성화하고자 할 때는pointer-events-none
속성을 추가한다.
완성된 페이지네이션 UI

다음과 같은 UI의 페이지네이션 기능이 완성되었다.
이제 검색 결과가 많더라도 빠르고 효율적으로 페이지를 나누어 조회할 수 있다.
✅ 검색 시 URL 업데이트 ?q=검색어&page=2
✅ Supabase range()
를 활용한 페이징 쿼리 적용
✅ < 1 2 3 4 5 > UI 구현