개발에서는 멀쩡하던 페이지가 빌드에서 useSearchParams로 실패한 이유
문제 발생
검색창 컴포넌트를 만들고 로컬에서 잘 확인한 뒤 배포했는데, 빌드가 실패했습니다.
Error: Missing Suspense boundary with useSearchParams"use client";
export function SearchBar() {
const searchParams = useSearchParams();
return <>Search: {searchParams.get("search")}</>;
}pnpm dev에서는 한 번도 본 적 없는 오류였습니다.
원인 분석
개발과 프로덕션의 렌더 방식이 다릅니다. Next.js 문서가 이 차이를 그대로 적습니다 — 개발에서는 라우트가 요청 시점에 렌더되므로 useSearchParams가 서스펜드하지 않고, Suspense 없이도 동작하는 것처럼 보입니다. 그리고 이어서 — 프로덕션 빌드에서는 Client Component에서 useSearchParams를 호출하는 정적 페이지가 반드시 Suspense 경계로 감싸져야 하며, 그렇지 않으면 빌드가 "Missing Suspense boundary with useSearchParams" 오류로 실패합니다.
이유는 쿼리 문자열이 빌드 시점에 알 수 없는 값이기 때문입니다. 문서 설명대로 라우트가 프리렌더되면 useSearchParams 호출은 가장 가까운 Suspense 경계까지의 Client Component 트리를 클라이언트 사이드 렌더로 돌립니다. 경계가 없으면 그 범위가 페이지 전체가 됩니다.
즉 경고의 목적은 문법 교정이 아니라 CSR로 내려가는 범위를 사람이 정하게 하는 것입니다.
해결 방안
- 훅을 쓰는 컴포넌트만 경계 안에 넣습니다. 문서 권고 그대로 — 그러면 위쪽 컴포넌트들은 프리렌더되어 초기 HTML에 실립니다.
<Suspense fallback={<SearchBarSkeleton />}>
<SearchBar />
</Suspense>-
Server Component 페이지라면 애초에 훅이 필요 없습니다. 문서가 먼저 권하는 방법입니다 — 페이지의
searchParamsprop을 읽어 필요한 값만 props로 내려보냅니다. 초기 HTML에 값이 들어가므로 SEO에도 유리합니다. -
레이아웃에서는
searchParams를 받을 수 없다는 것도 함께 기억합니다. 문서가 이유까지 적습니다 — 공유 레이아웃은 내비게이션마다 다시 렌더되지 않아 값이 낡을 수 있기 때문입니다. 레이아웃에서 쿼리가 필요하면 그 조각만 Client Component로 뺍니다. -
정말 라우트 전체가 동적이어야 한다면 그 의도를 명시합니다. 문서는
export const dynamic = "force-dynamic"대신 Server Component에서connection()을 먼저 호출하는 쪽을 권합니다 — 동적 렌더링을 "들어온 요청"에 의미상 연결하기 때문입니다. -
fallback을 성의 있게 만듭니다. 이 경계는 실제로 사용자가 보는 첫 화면입니다. 빈
null을 넣으면 검색창이 늦게 튀어나오는 레이아웃 이동이 생깁니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.