Next.js, Supabase & Vercel ISR로 137K 리스팅 글로벌 디렉토리 구축하기
지난해 우리는 137,000개의 리스팅이 있는 글로벌 디렉토리를 출시했습니다. 프로토타입이 아닙니다. "나중에 최적화하자"는 MVP도 아닙니다. 수백만 페이지 뷰를 처리하고, 수천 개의 롱테일 키워드로 순위를 매기며, 서버에 부담 없이 페이지를 온디맨드로 재생성하는 프로덕션 시스템입니다. 이것이 우리가 어떻게 이를 구축했는지, 그리고 이를 가능하게 한 아키텍처 결정의 이야기입니다.
스택: Next.js 14 (App Router), Supabase (PostgreSQL + Edge Functions), Vercel (호스팅 + ISR), 그리고 실용성의 건강한 복합입니다. 우리는 실수했습니다. 막힌 벽을 만났습니다. 끝난 줄 알았던 것을 다시 작성했습니다. 하지만 최종 아키텍처는 137,000+개의 동적 페이지를 전 세계적으로 200ms 미만의 TTFB로 처리하며, Supabase 청구서는 월 $100 이하로 유지됩니다.
유사한 것을 구축하고 있다면 — 마켓플레이스, 디렉토리, 리스팅 플랫폼 — 이것이 우리가 시작할 때 존재했으면 하는 기사입니다.
목차
- 이 스택을 선택한 이유
- 데이터 레이어: 대규모의 Supabase
- 페이지 생성 전략: ISR, SSG, 그리고 137K 문제
- URL 아키텍처와 대규모 SEO
- 검색 및 필터링: 어려운 부분
- 성능 예산 및 엣지 캐싱
- 프로덕션 모니터링 및 관찰성
- 비용 분석: 실제 소비 비용
- 다르게 할 것들
- FAQ

이 스택을 선택한 이유
Next.js + Supabase + Vercel로 최종 결정하기 전에 많은 옵션을 평가했습니다. 핵심 요구사항은 다음과 같았습니다:
- 137,000+개의 고유 페이지 검색 엔진이 크롤링하고 인덱싱할 수 있는
- 1초 이하의 페이지 로드 전 세계적으로 (40개국 이상의 사용자)
- 동적 데이터 — 리스팅은 매일 업데이트되고, 일부는 시간마다
- 전체 텍스트 검색 패싯 필터링 포함
- 예산 친화적 — 이것은 VC 자금 지원을 받은 달 착한 사업이 아니었습니다
우리는 Astro를 검토했습니다(정적 사이트에는 훌륭하지만, 더 많은 동적 상호 작용이 필요했습니다 — 비록 우리 Astro 개발 팀이 우수한 디렉토리 프로젝트를 많이 출시했지만). WordPress + WPEngine을 살펴봤습니다. Algolia와 함께 순수 SPA도 잠깐 검토했습니다.
Next.js가 이겼습니다. 하나의 킬러 기능 때문입니다: 증분 정적 재생성. ISR은 정적 성능과 동적 콘텐츠 중 하나를 선택할 필요가 없다는 의미였습니다. 둘 다 가질 수 있었습니다.
Supabase는 PlanetScale과 Neon을 이겼습니다. 완전한 패키지 때문입니다 — auth, storage, edge functions, 그리고 Row Level Security를 갖춘 진정으로 좋은 Postgres 구현. 디렉토리의 경우, 모두 필요합니다.
Vercel은 배포 대상으로 선택되었습니다. ISR은 Vercel에서 최고로 작동하기 때문입니다(당연하게도). 통합이 기본입니다. 온디맨드 재검증이 그냥 작동합니다.
자체 호스팅은 어떤가요?
우리는 Railway의 자체 호스팅 Next.js 설정을 프로토타이핑했습니다. 작동했지만, 자체 호스팅 Next.js의 ISR에는 이상한 점이 있습니다. 캐시 무효화 이야기는 더 나쁩니다. 자신의 CDN 레이어를 관리해야 합니다. 3명의 엔지니어 팀의 경우, 우리가 절약할 $200/월을 위한 운영 오버헤드는 가치가 없었습니다.
데이터 레이어: 대규모의 Supabase
우리의 Supabase 데이터베이스는 각각 40-60개의 필드를 가진 137,000개의 리스팅을 보유합니다. 카테고리, 위치, 연락처 정보, 풍부한 설명, 이미지, 평점, 영업 시간 — 모든 것.
스키마 설계
가장 큰 결정은 정규화된 관계형 스키마를 사용할지 아니면 JSONB 열이 있는 더 문서 지향적인 접근 방식을 사용할지였습니다. 우리는 하이브리드를 사용했습니다:
CREATE TABLE listings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
description TEXT,
category_id UUID REFERENCES categories(id),
city_id UUID REFERENCES cities(id),
country_code TEXT NOT NULL,
coordinates GEOGRAPHY(POINT, 4326),
contact JSONB DEFAULT '{}',
attributes JSONB DEFAULT '{}',
media JSONB DEFAULT '[]',
rating_avg NUMERIC(3,2) DEFAULT 0,
rating_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
published_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT NOW(),
search_vector TSVECTOR
);
CREATE INDEX idx_listings_category ON listings(category_id) WHERE status = 'active';
CREATE INDEX idx_listings_city ON listings(city_id) WHERE status = 'active';
CREATE INDEX idx_listings_country ON listings(country_code) WHERE status = 'active';
CREATE INDEX idx_listings_coordinates ON listings USING GIST(coordinates);
CREATE INDEX idx_listings_search ON listings USING GIN(search_vector);
CREATE INDEX idx_listings_slug ON listings(slug);
필터링하는 항목에 대한 구조화된 관계형 데이터(카테고리, 도시, 국가). 리스팅당 다르게 변하는 반구조화된 것(연락처 방법, 사용자 정의 속성, 미디어 배열)에 대한 JSONB. 이것은 우리에게 최고의 결과를 주었습니다 — 관계형 열에 대한 빠른 인덱싱된 쿼리와 나머지 부분의 유연성.
검색 벡터
그 search_vector 열은 중요합니다. 우리는 트리거로 채웁니다:
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.attributes->>'keywords', '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
이는 모든 리스팅이 Postgres 자체를 통해 전체 텍스트 검색 가능하다는 의미입니다. 처음 100K 리스팅에 외부 검색 서비스가 필요하지 않습니다. 나중에 이것이 언제 깨지는지 이야기하겠습니다.
연결 풀링
Supabase는 연결 풀링에 PgBouncer를 사용합니다. ISR을 사용하면 서버리스 함수 호출의 버스트가 발생합니다 — 각 함수는 데이터베이스 연결이 필요합니다. 풀링이 없으면, 몇 분 내에 연결이 소진됩니다.
우리는 모든 서버리스 컨텍스트에 대해 풀된 연결 문자열(port 6543)을 사용하고 마이그레이션 및 관리 작업에만 직접 연결(port 5432)을 사용합니다. 이것은 명백해 보이지만 사람들을 사로잡는 것 중 하나입니다.
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Server-side only
{
db: { schema: 'public' },
auth: { persistSession: false }
}
)
페이지 생성 전략: ISR, SSG, 그리고 137K 문제
여기서 일이 흥미로워집니다. 그리고 우리가 가장 큰 초기 실수를 한 곳입니다.
순진한 접근법(이것을 하지 마세요)
우리의 첫 번째 시도: generateStaticParams를 사용하여 모든 137,000개 페이지를 빌드 시간에 생성합니다. 빌드는 4시간 22분이 걸렸습니다. Vercel의 무료 계층은 45분 빌드 제한이 있습니다. Pro 계층도 6시간으로 상한선이 있습니다. 하지만 실제 문제는 타임아웃이 아니었습니다 — 피드백 루프였습니다. 모든 배포는 반나절이 걸렸습니다. 그것은 작동하지 않습니다.
ISR 접근법(실제로 작동하는 것)
출시된 전략은 다음과 같습니다:
- 빌드 시간에: 상위 5,000개 페이지(트래픽 기준)를 정적으로 생성
- 첫 요청 시: 나머지 페이지를 온디맨드로 생성하고 캐시
- 재검증: 시간 기반(매 3600초) + 웹훅을 통한 온디맨드
// app/listing/[slug]/page.tsx
import { supabase } from '@/lib/supabase'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
// Only pre-generate top listings by traffic
const { data } = await supabase
.from('listings')
.select('slug')
.eq('status', 'active')
.order('rating_count', { ascending: false })
.limit(5000)
return (data || []).map((listing) => ({
slug: listing.slug,
}))
}
export const revalidate = 3600 // Revalidate every hour
export default async function ListingPage({ params }: { params: { slug: string } }) {
const { data: listing, error } = await supabase
.from('listings')
.select(`
*,
category:categories(*),
city:cities(*, country:countries(*))
`)
.eq('slug', params.slug)
.eq('status', 'active')
.single()
if (!listing || error) notFound()
return <ListingDetail listing={listing} />
}
온디맨드 재검증
리스팅 소유자가 데이터를 업데이트할 때, 우리는 최대 1시간 동안 페이지 새로고침을 기다리고 싶지 않습니다. Supabase 웹훅이 Next.js API 라우트를 트리거합니다:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidation-secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { slug, type } = await request.json()
if (type === 'listing') {
revalidatePath(`/listing/${slug}`)
revalidatePath(`/`) // Revalidate homepage too
}
return NextResponse.json({ revalidated: true })
}
이것은 우리에게 최고의 이점을 줍니다: 동적 사이트 신선도가 있는 정적 사이트 성능. 빌드는 8분 이내에 완료됩니다. 사전 생성되지 않은 페이지는 첫 방문할 때 생성되고 엣지에서 캐시됩니다.
숫자들
| 메트릭 | 전체 SSG (순진함) | ISR (프로덕션) |
|---|---|---|
| 빌드 시간 | 4h 22m | 7m 40s |
| 배포 시 페이지 | 137,000 | 5,000 |
| 첫 방문(캐시되지 않음) | N/A | ~800ms |
| 후속 방문 | ~120ms | ~120ms |
| 재검증 지연 시간 | 전체 재배포 | < 2 seconds |
| 월간 빌드 분 | 한계 초과 | ~230 minutes |

URL 아키텍처와 대규모 SEO
137,000개의 페이지를 보유하면, URL 구조는 사후 생각이 아닙니다 — 아키텍처입니다. 모든 URL은 순위 기회입니다.
URL 계층
/ → Homepage
/categories/[category-slug] → Category pages (48 categories)
/locations/[country]/[city] → Location pages
/listing/[listing-slug] → Individual listing
/search?q=...&category=...&city=... → Search results (noindex)
카테고리 + 위치 교차점 페이지는 실제 SEO 금광입니다:
/categories/restaurants/us/new-york → "Restaurants in New York"
/categories/hotels/uk/london → "Hotels in London"
이 교차점 페이지는 ISR로 동적으로 생성됩니다. 대략 12,000개의 유효한 조합이 있습니다. 각각은 특정 롱테일 키워드를 목표로 합니다.
사이트맵 생성
137K URL을 사용하면, 사이트맵 인덱스 파일이 필요합니다. Google의 제한은 사이트맵당 50,000 URL입니다.
// app/sitemap/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const page = parseInt(params.id)
const perPage = 45000 // Stay under the 50K limit
const offset = page * perPage
const { data: listings } = await supabase
.from('listings')
.select('slug, updated_at')
.eq('status', 'active')
.order('id')
.range(offset, offset + perPage - 1)
const xml = generateSitemapXml(listings)
return new Response(xml, {
headers: { 'Content-Type': 'application/xml' },
})
}
우리는 4개의 사이트맵으로 나눕니다: sitemap-0.xml부터 sitemap-3.xml까지, 사이트맵 인덱스에서 참조합니다. Google Search Console은 제출된 URL의 98%를 6주 내에 인덱싱했습니다.
구조화된 데이터
모든 리스팅 페이지에는 JSON-LD 구조화된 데이터가 포함됩니다. 디렉토리의 경우, LocalBusiness 스키마는 중요합니다:
const structuredData = {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: listing.title,
description: listing.description,
address: {
'@type': 'PostalAddress',
addressLocality: listing.city.name,
addressCountry: listing.city.country.code,
},
geo: {
'@type': 'GeoCoordinates',
latitude: listing.coordinates?.lat,
longitude: listing.coordinates?.lng,
},
aggregateRating: listing.rating_count > 0 ? {
'@type': 'AggregateRating',
ratingValue: listing.rating_avg,
reviewCount: listing.rating_count,
} : undefined,
}
검색 및 필터링: 어려운 부분
검색은 항상 어려운 부분입니다. 항상.
1단계: Postgres 전체 텍스트 검색
초기 출시의 경우, Postgres tsvector 검색이 모든 것을 처리했습니다. GIN 인덱스가 있는 137K 행에 충분히 빠릅니다. 쿼리 시간은 평균 40-80ms였습니다.
const { data } = await supabase
.from('listings')
.select('id, slug, title, description, category:categories(name)')
.textSearch('search_vector', query, { type: 'websearch' })
.eq('status', 'active')
.eq('country_code', countryFilter)
.order('rating_avg', { ascending: false })
.range(0, 19)
2단계: Postgres가 충분하지 않을 때
약 80,000개의 리스팅에서, 복잡한 패싯 검색(카테고리 + 위치 + 텍스트 + 정렬)은 300-500ms를 시작했습니다. 대부분의 앱에는 허용되지만, 우리의 사용자는 즉각적인 결과를 기대했습니다.
우리는 검색 레이어로 Typesense를 추가했습니다. Algolia가 아닙니다(우리 규모에서는 너무 비쌉니다 — 우리는 월 $500+를 지불해야 할 것 같습니다). Meilisearch가 아닙니다(훌륭하지만, Typesense의 지리 검색이 우리 사용 사례에 더 좋았습니다).
Typesense는 단일 $48/월 Hetzner 인스턴스에서 실행됩니다. Supabase에서 매일 밤 전체 인덱스 재생성 + 실시간 웹훅 업데이트를 통해 동기화합니다. 검색 쿼리는 이제 평균 8-15ms입니다.
| 검색 솔루션 | 쿼리 시간 (p50) | 쿼리 시간 (p99) | 월간 비용 | 패싯 검색 |
|---|---|---|---|---|
| Postgres FTS | 45ms | 320ms | $0 (included) | Limited |
| Typesense | 9ms | 28ms | $48 | Excellent |
| Algolia | ~5ms | ~15ms | $500+ | Excellent |
| Meilisearch | ~8ms | ~22ms | $48 (자체 호스팅) | Good |
성능 예산 및 엣지 캐싱
우리는 처음부터 공격적인 성능 목표를 설정했습니다:
- TTFB: < 200ms (전 세계 p75)
- LCP: < 1.5s
- CLS: < 0.05
- 총 페이지 무게: < 300KB (초기 로드)
Vercel 엣지 네트워크
ISR 페이지는 Vercel의 엣지 네트워크에 캐시됩니다 — 100개 이상의 PoP가 전 세계적으로. 페이지가 생성되고 캐시되면, 가장 가까운 엣지 위치에서 제공됩니다. 이것이 동남아시아나 남미의 사용자도 TTFB가 200ms 미만으로 유지되는 이유입니다.
이미지 최적화
각 리스팅에는 1-8개의 이미지가 있습니다. 이것은 잠재적으로 100만 개 이상의 이미지입니다. 우리는 next/image로 Vercel의 기본 제공 이미지 최적화를 사용합니다:
<Image
src={listing.media[0]?.url}
alt={listing.title}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading={index === 0 ? 'eager' : 'lazy'}
quality={75}
/>
이미지는 Supabase Storage에 저장되고 Vercel의 이미지 CDN을 통해 제공됩니다. 원본 이미지는 종종 2-5MB입니다. 최적화 후 40-120KB입니다. 이것 혼자 대역폭의 대략 80%를 절약했습니다.
프로덕션 모니터링 및 관찰성
137K 페이지를 모니터링 없이 프로덕션에서 실행하는 것은 눈이 먼 상태로 운전하는 것과 같습니다. 우리의 스택은 다음과 같습니다:
- Vercel Analytics: Core Web Vitals, 실제 사용자 모니터링
- Sentry: 오류 추적(우리는 매일 약 50개의 오류를 포착합니다, 대부분 봇이 보내는 쓰레기)
- Supabase Dashboard: 데이터베이스 성능, 쿼리 분석
- Checkly: 합성 모니터링, 중요 경로에서 5분 간격
- Google Search Console: 인덱스 적용 범위, 크롤 통계
우리가 설정한 가장 가치 있는 모니터링은 인덱싱된 페이지를 총 활성 리스팅과 비교하는 일일 Supabase 쿼리였습니다. 비율이 95% 아래로 떨어지면, 우리는 알림을 받습니다. 이것은 나쁜 변경 배포 24시간 내에 사이트맵 재발을 포착했습니다.
비용 분석: 실제 소비 비용
사람들은 항상 비용을 묻습니다. 2025년 Q1 기준 실제 월간 지출은 다음과 같습니다:
| 서비스 | 플랜 | 월간 비용 |
|---|---|---|
| Vercel | Pro | $20 |
| Vercel 대역폭(초과) | 종량제 | ~$35 |
| Supabase | Pro | $25 |
| Supabase 데이터베이스(컴퓨팅) | 작은 인스턴스 | $48 |
| Typesense (Hetzner) | CX31 | $48 |
| Checkly | 시작 | $7 |
| Sentry | 팀 | $26 |
| 도메인 + DNS (Cloudflare) | 무료 계층 | $0 |
| 합계 | ~$209/month |
137,000개 페이지를 약 $200/월로 수백만 월간 페이지 뷰를 제공합니다. WordPress를 실행하는 전통적인 서버 설정으로 이를 수행하려고 시도하십시오.
유사한 프로젝트를 검토 중이고 이와 같은 아키텍처가 예산에 어떻게 매핑되는지 이해하고 싶다면, 우리의 가격 페이지는 우리가 일반적으로 디렉토리 및 마켓플레이스 프로젝트의 범위를 어떻게 정하는지 분석합니다.
다르게 할 것들
처음부터 ISR을 시작하십시오. 우리는 전체 SSG를 작동하려고 시도하는 데 2주를 낭비했습니다. 그 수학이 작동하지 않는다는 것을 받아들이기 전까지.
처음부터 Typesense를 사용하세요. Postgres FTS는 초기에는 괜찮았지만, 프로젝트 중간에 검색을 마이그레이션하는 것은 혼란스러웠습니다. $48/월은 출시부터 가치가 있었을 것입니다.
이전에 데이터 검증에 투자하세요. 다양한 소스에서 가져온 137K 리스팅으로, 데이터 품질은 악몽이었습니다. 우리는 첫 번째 가져오기 전이 아니라, 프로덕션에서 수천 개의 손상된 레코드를 찾은 후에 더 엄격한 Zod 스키마와 검증 파이프라인을 구축했어야 했습니다.
스테이징에서 현실적인 데이터 볼륨으로 테스트하세요. 우리의 스테이징 환경에는 500개의 리스팅이 있었습니다. 500개 행에서 잘 작동하는 쿼리는 137K에서 떨어졌습니다. 우리는 이제 프로덕션 데이터의 20% 무작위 샘플로 스테이징을 시드합니다.
비슷한 디렉토리나 마켓플레이스 빌드를 계획 중이고 이 같은 함정을 피하고 싶다면, 우리 팀에 연락하세요. 우리는 이것을 충분히 하여 지뢰가 어디에 있는지 알고 있습니다.
FAQ
Next.js로 100K+ 리스팅 디렉토리를 구축하는 데 얼마나 오래 걸리나요?
우리 팀의 경우, 초기 아키텍처와 핵심 기능은 약 10주가 걸렸습니다. 데이터 가져오기, 청소 및 검증에는 추가 3-4주가 추가되었습니다. 킥오프에서 프로덕션 출시까지의 총 시간은 대략 14주였습니다. Next.js 개발 팀과 함께 이것을 한 번 해봤다면, 2-3주를 깎을 수 있습니다.
Supabase가 디렉토리에 100,000+행을 처리할 수 있나요?
절대입니다. Supabase는 Postgres에서 실행되며, 수백만 행을 아무 힘 없이 처리합니다. 핵심은 적절한 인덱싱입니다 — 가장 많이 쿼리되는 열에 인덱스가 없으면, 성능이 빠르게 저하됩니다. 위에서 설명한 인덱스를 사용하면, 137K 행에 대한 우리의 쿼리는 단일 레코드 조회에 대해 지속적으로 50ms 미만으로 반환됩니다.
큰 사이트에서 ISR과 SSG의 차이점은 무엇인가요?
SSG (정적 사이트 생성)는 배포 시간에 모든 페이지를 구축합니다. ISR (증분 정적 재생성)은 배포 시간에 부분 집합을 구축하고 나머지는 온디맨드로 생성합니다. 약 10,000개 이상의 페이지가 있는 사이트의 경우, ISR은 실제로 필요합니다 — 전체 SSG 빌드가 합리적인 배포 주기에 대해 너무 느려집니다.
137,000개의 동적 생성 페이지에 대해 SEO를 어떻게 처리하나요?
가장 중요한 세 가지: 여러 파일로 분할된 적절한 사이트맵 생성, 모든 리스팅 페이지의 고유한 구조화된 데이터(JSON-LD), ISR 생성 페이지가 적절한 HTTP 200 상태 코드를 반환하는 것 보장(소프트 404 없음). 우리는 또한 리스팅 데이터를 사용하여 모든 페이지마다 고유한 메타 제목과 설명을 생성합니다 — 중복 메타 콘텐츠 없음.
Vercel ISR이 프로덕션 규모에서 신뢰할 수 있나요?
우리의 경험상, 그렇습니다. 우리는 8개월 이상 이 설정을 99.98% 가동률로 실행했습니다. 유일한 사건은 자체 초래했습니다 — 재검증 웹훅을 깨뜨린 나쁜 배포, 그리고 하나의 Supabase 유지 보수 창이 15분의 저하된 검색을 일으켰습니다. Vercel의 엣지 캐시는 견고합니다.
큰 디렉토리의 경우 Algolia 또는 Typesense를 사용해야 하나요?
예산에 따라 다릅니다. Algolia는 최고의 개발자 경험을 갖춘 업계 표준이지만, 100K 레코드를 넘으면 비용이 많이 듭니다 — 월 $500-1000+ 예상합니다. Typesense는 자체 호스팅될 때 비용의 일부로 기능의 90%를 제공합니다. 우리는 Typesense를 선택했고 후회하지 않았습니다.
137,000개의 리스팅을 어떻게 최신 상태로 유지하나요?
우리는 조합된 접근 방식을 사용합니다: 개별 리스팅 변경에 의해 트리거되는 온디맨드 재검증(Supabase 웹훅), 안전 장치로 시간 기반 ISR 재검증(시간별), 그리고 매일 밤 일괄 작업은 오래된 데이터를 확인하고 일괄 재검증을 트리거합니다. 리스팅 소유자는 대시보드를 통해 페이지 새로고침을 수동으로 요청할 수도 있습니다.
이 아키텍처가 Supabase 대신 헤드리스 CMS와 함께 작동할 수 있나요?
네, 절충점이 있습니다. 헤드리스 CMS 설정 Sanity 또는 Contentful처럼 콘텐츠 관리 측면에서 잘 작동하지만, 검색 및 복잡한 쿼리를 위해 여전히 데이터베이스가 필요합니다. 우리는 편집 콘텐츠가 헤드리스 CMS에 있고 리스팅 데이터가 Postgres에 있는 디렉토리 프로젝트를 구축했습니다 — 유효한 하이브리드 접근입니다.