master-project
CHAPTER 18 / 50
읽기 약 2분
FUNCTION
검색 + 필터 + 정렬 + 페이지네이션
핵심 개념
URL 기반 상태 + Debounce + 전문 검색 + 정렬 옵션 + 무한 스크롤 — 실전 목록 페이지의 핵심.
본문
URL 기반 상태 (공유 가능)
// src/app/(app)/history/page.tsx
export default async function HistoryPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; sort?: string; filter?: string; cursor?: string }>
}) {
const params = await searchParams
const supabase = await createClient()
let q = supabase.from('generations').select('*')
// 검색
if (params.q) q = q.ilike('prompt', `%${params.q}%`)
// 필터
if (params.filter === 'today') {
const today = new Date().toISOString().split('T')[0]
q = q.gte('created_at', today)
}
// 정렬
const sortBy = params.sort === 'oldest' ? { ascending: true } : { ascending: false }
q = q.order('created_at', sortBy)
// 페이지네이션
if (params.cursor) q = q.lt('created_at', params.cursor)
q = q.limit(20)
const { data: gens } = await q
return <HistoryClient gens={gens} params={params} />
}검색 박스 (Debounce)
// src/components/search-input.tsx
'use client'
import { useEffect, useState } from 'react'
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
import { Search } from 'lucide-react'
export function SearchInput() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [value, setValue] = useState(searchParams.get('q') ?? '')
useEffect(() => {
const t = setTimeout(() => {
const params = new URLSearchParams(searchParams)
if (value) params.set('q', value)
else params.delete('q')
router.replace(`${pathname}?${params.toString()}`)
}, 300)
return () => clearTimeout(t)
}, [value])
return (
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
value={value}
onChange={e => setValue(e.target.value)}
placeholder="검색..."
className="w-full pl-9 pr-3 py-2 border rounded-md"
/>
</div>
)
}정렬·필터 드롭다운
'use client'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
import { ChevronDown } from 'lucide-react'
export function SortDropdown() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const current = searchParams.get('sort') ?? 'newest'
const setSort = (sort: string) => {
const params = new URLSearchParams(searchParams)
params.set('sort', sort)
router.replace(`${pathname}?${params.toString()}`)
}
return (
<DropdownMenu>
<DropdownMenuTrigger className="border px-3 py-1.5 rounded text-sm flex items-center gap-1">
{current === 'newest' ? '최신순' : '오래된순'} <ChevronDown className="h-3 w-3" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setSort('newest')}>최신순</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSort('oldest')}>오래된순</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}전문 검색 (PostgreSQL FTS)
-- 마이그레이션
alter table generations add column search_vec tsvector
generated always as (to_tsvector('simple', coalesce(prompt,'') || ' ' || coalesce(result,''))) stored;
create index idx_gens_fts on generations using gin(search_vec);// 검색 사용
const { data } = await supabase
.from('generations')
.select()
.textSearch('search_vec', `${query}:*`)무한 스크롤 (IntersectionObserver)
'use client'
import { useEffect, useRef } from 'react'
export function LoadMoreSentinel({ onLoad }: { onLoad: () => void }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const io = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) onLoad()
},
{ rootMargin: '100px' }
)
if (ref.current) io.observe(ref.current)
return () => io.disconnect()
}, [onLoad])
return <div ref={ref} className="h-10" />
}결과 없음 (No Results)
{gens && gens.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground">검색 결과가 없습니다.</p>
<button onClick={resetFilters} className="text-primary hover:underline mt-2">
필터 초기화
</button>
</div>
)}다음 챕터
CH.19 "파일 업로드 UI: 드래그앤드롭".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 마스터 프로젝트의 검색·필터·페이지네이션 부분을 분석해서 실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
검색·필터·페이지네이션 관련 모범 사례·안티패턴 5개를 비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 검색·필터·페이지네이션 최적화 가능 위치와 리스크를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 1인 개발자 시장의 검색·필터·페이지네이션 트렌드와 차별화 포인트를 정리해줘.
⭐ 이것만 기억하세요
검색 + 필터 + 정렬 + 페이지네이션은 이 3가지만 확실히 잡으세요
1.URL searchParams = 공유 가능한 검색 상태
2.PostgreSQL FTS = 빠른 전문 검색
3.다음 챕터에서 파일 업로드
공유하기
진행도 18 / 50