본문으로 건너뛰기
개발 머꼬
개발 노트Next.js
hohyeon.dev19

개발에서는 멀쩡하던 페이지가 빌드에서 useSearchParams로 실패한 이유

  • #Common Pitfall
  • #Engineering Note
  • #Next.js

문제 발생

검색창 컴포넌트를 만들고 로컬에서 잘 확인한 뒤 배포했는데, 빌드가 실패했습니다.

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로 내려가는 범위를 사람이 정하게 하는 것입니다.

해결 방안

  1. 훅을 쓰는 컴포넌트만 경계 안에 넣습니다. 문서 권고 그대로 — 그러면 위쪽 컴포넌트들은 프리렌더되어 초기 HTML에 실립니다.
<Suspense fallback={<SearchBarSkeleton />}>
  <SearchBar />
</Suspense>
  1. Server Component 페이지라면 애초에 훅이 필요 없습니다. 문서가 먼저 권하는 방법입니다 — 페이지의 searchParams prop을 읽어 필요한 값만 props로 내려보냅니다. 초기 HTML에 값이 들어가므로 SEO에도 유리합니다.

  2. 레이아웃에서는 searchParams를 받을 수 없다는 것도 함께 기억합니다. 문서가 이유까지 적습니다 — 공유 레이아웃은 내비게이션마다 다시 렌더되지 않아 값이 낡을 수 있기 때문입니다. 레이아웃에서 쿼리가 필요하면 그 조각만 Client Component로 뺍니다.

  3. 정말 라우트 전체가 동적이어야 한다면 그 의도를 명시합니다. 문서는 export const dynamic = "force-dynamic" 대신 Server Component에서 connection()을 먼저 호출하는 쪽을 권합니다 — 동적 렌더링을 "들어온 요청"에 의미상 연결하기 때문입니다.

  4. fallback을 성의 있게 만듭니다. 이 경계는 실제로 사용자가 보는 첫 화면입니다. 빈 null을 넣으면 검색창이 늦게 튀어나오는 레이아웃 이동이 생깁니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.