TechFeedTechFeed
Frontend

useSearchParams, Suspense, missing boundary | 빌드만 멈추면?

missing suspense boundary는 훅이 잘못된 게 아니라 useSearchParams가 정적 빌드에서 쿼리를 읽는 동안 대체 화면이 없어 넥스트가 멈춘 줄입니다. 쿼리 컴포넌트만 Suspense로 감싸고 루트 전체를 덮지 않습니다. Next.js App Router, 한국 1인 개발자 기준. 2026년 9월 Next.js missing suspense 문서.

by

missing suspense boundary는 훅이 잘못된 게 아니라, useSearchParams가 클라이언트에서 쿼리를 읽는 동안 서버가 미리 그릴 대체 화면이 없어서 넥스트가 빌드를 멈춘 줄입니다. 개발 서버에선 넘어가다가 next build만 빨간 줄이 납니다.


페이지를 정적으로 뽑으려다 검색 파라미터가 끼면 렌더가 요청마다 달라집니다. 그 경계를 감싸지 않으면 빌드가 CSR 베일을 허용하지 않아요.


페이지 전체가 아니라 쿼리를 읽는 컴포넌트만 Suspense로 감싸면 됩니다. 공식 메시지 문서에 같은 처방이 적혀 있습니다. 근거는 Next.js missing suspense 문서useSearchParams에 있습니다.


왜 개발은 되고 빌드만 멈추나

빌드가 정적 페이지를 미리 그리려다 막혀서입니다. 개발 모드(next dev)는 요청마다 그리므로 쿼리 훅이 있어도 당장 실패하지 않습니다. 프로덕션 빌드는 가능한 경로를 HTML로 뽑고, 그때 클라이언트 전용 훅이 끼면 CSR 베일아웃을 선언합니다.


넥스트 공식 메시지는 이 상황을 "정적 렌더 중 클라이언트 사이드 베일을 감쌀 Suspense가 없다"고 설명합니다. useSearchParams 말고도 일부 클라이언트 훅이 같은 줄을 냅니다. 콘솔에 컴포넌트 스택이 붙으니, 그 파일부터 열면 됩니다.


하이드레이션 미스매치와는 다른 칸입니다. 미스매치는 서버 HTML과 브라우저 첫 그림이 다를 때고, 여긴 빌드가 정적 산출물을 만들지 못할 때입니다. 화면이 깜빡이는 줄은 하이드레이션 글을 보세요.


빌드 로그의 파일 경로 | 에러 아래에 쿼리를 읽은 클라이언트 컴포넌트 경로가 있습니다. 페이지.tsx 전체가 아니라 그 컴포넌트를 Suspense로 감싸는 게 최소 수정입니다.


next build가 useSearchParams를 만나 Suspense 없이 정적 렌더를 멈추는 개념 이미지
개발 서버는 넘기고, 정적 빌드만 경계를 요구한다

useSearchParams가 정적 렌더를 깨는 이유

쿼리는 요청마다 달라서 미리 그릴 값이 없습니다. 빌드 시점에 ?tab=review가 있을지 넥스트는 모릅니다. 그래서 그 훅을 쓰는 트리는 클라이언트에서 채울 때까지 비워 둬야 합니다.


코드빌드고침
클라이언트에서 useSearchParamsSuspense 없으면 실패그 컴포넌트를 fallback과 함께 감싸기
서버 페이지 searchParams props동적 렌더로 전환페이지를 동적으로 두거나 쿼리를 서버에서 읽기
usePathname만대개 통과경로만 필요하면 이 훅 유지
useRouter().query (pages)App Router와 다른 칸pages면 이 글 대상 아님

App Router 서버 컴포넌트는 페이지 props의 searchParams로 읽을 수 있습니다. 넥스트 15에서는 이 값도 Promise라 await가 필요할 수 있어요. 클라이언트 훅이 꼭 필요하면 Suspense가 답입니다.


Suspense로 감싸는 최소 고침

쿼리를 읽는 컴포넌트만 감싸면 됩니다. 공식 예제도 필터 바 같은 작은 경계를 권합니다. fallback은 같은 높이의 스켈레톤이면 레이아웃이 덜 흔들립니다.


쿼리 컴포넌트만 Suspense
// app/shop/page.js import { Suspense } from 'react' import { Filters } from './filters' export default function ShopPage() { return ( <main> <h1>상품</h1> <Suspense fallback={<p>필터 불러오는 중</p>}> <Filters /> </Suspense> </main> ) } // app/shop/filters.js 'use client' import { useSearchParams } from 'next/navigation' export function Filters() { const params = useSearchParams() const q = params.get('q') || '' return <p>검색어: {q}</p> }

Filters는 클라이언트 컴포넌트여야 훅을 씁니다. 페이지는 서버로 남겨 두면 본문 HTML은 빌드에 남고, 쿼리 부분만 클라이언트가 채웁니다. fallback 문구를 비우면 빌드는 통과해도 사용자가 빈 칸을 봅니다.


useSearchParams를 쓰는 작은 컴포넌트를 Suspense로 감싸 빌드를 통과시키는 구조 이미지
페이지 전체가 아니라 쿼리를 읽는 칸만 감싼다

페이지 전체를 감싸면 안 되는 이유

루트 레이아웃을 Suspense로 덮으면 정적 본문까지 같이 미뤄집니다. 빌드는 통과할 수 있어도 첫 페인트가 늦어요. 검색 파라미터와 무관한 제목·본문은 서버 HTML에 남겨 두는 편이 낫습니다.


export const dynamic = 'force-dynamic'으로 페이지를 아예 동적으로 두는 방법도 있습니다. 쿼리가 핵심인 관리자 화면이면 이 선택이 단순합니다. 마케팅 랜딩처럼 캐시가 중요한 페이지에 쿼리 필터 하나 있다고 전체를 동적으로 바꾸면 HTML 캐시가 사라집니다.


에러 바운더리와 혼동하지 마세요. error.js는 런타임 예외용이고, Suspense는 아직 안 온 데이터를 기다리는 칸입니다. 빌드가 컴파일 에러로 죽으면 이 글 대상이 아닙니다. 전역 에러 화면은 넥스트 에러 처리 글을 보세요.


  • [ ] 빌드 로그에서 useSearchParams를 쓴 파일을 찾았다
  • [ ] 그 컴포넌트만 client로 두고 Suspense로 감쌌다
  • [ ] fallback에 같은 높이 자리를 넣었다
  • [ ] 루트 레이아웃 전체를 감싸지 않았다
  • [ ] 쿼리가 핵심인 페이지만 force-dynamic을 검토했다

동적 강제보다 경계 | 필터 하나 때문에 페이지 전체를 force-dynamic으로 두면 캐시가 통째로 빠집니다. 훅을 쓰는 칸만 감싸는 쪽이 기본입니다.


빌드 로그로 범인 컴포넌트 찾기

스택 맨 위 파일부터 엽니다. 공유 UI 키트에 훅이 들어 있으면 여러 페이지 빌드가 한꺼번에 죽습니다. 그 키트를 쓰는 모든 페이지에 Suspense를 반복하기보다, 키트 바깥에서 한 번 감싸는 래퍼를 두는 편이 낫습니다.


로컬에서 next build를 돌려 재현하세요. 개발 서버만 보면 이 줄은 안 나옵니다. 힙이 모자란 빌드 실패와 메시지가 다르니, 메모리 부족은 heap out of memory 글로 나누세요.


한국에서 넥스트 앱 라우터로 목록+필터를 만들면, 필터 바를 레이아웃에 직접 넣는 순간 이 줄을 만납니다. 레이아웃은 감싸고, 필터는 자식 슬롯에서 Suspense로 여는 구조가 안전합니다.


next build 로그에서 useSearchParams 컴포넌트 경로를 찾아 Suspense 경계를 넣는 흐름 이미지
개발 서버가 아니라 프로덕션 빌드로 재현한다

참고 자료


내부 연계: 하이드레이션 미스매치, 넥스트 에러 처리, 빌드 메모리 부족


인용한 동작은 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

개발 서버에선 되는데 왜 빌드만 실패하나요?

개발은 요청마다 그리고, 빌드는 정적 HTML을 미리 뽑습니다. 쿼리 훅은 미리 그릴 값이 없어 CSR 베일이 필요하고, 그 경계를 안 감싸면 빌드가 멈춥니다.


페이지를 force-dynamic으로 두면 끝나나요?

그 페이지는 통과합니다. 캐시와 정적 산출물은 포기하게 됩니다. 필터 칸만 Suspense로 감싸는 쪽이 기본이고, 관리자처럼 매 요청이 다른 화면만 동적으로 두세요.


서버 컴포넌트에서 쿼리를 읽을 수는 없나요?

페이지·레이아웃 props의 searchParams로 읽습니다. 넥스트 15는 이 값이 Promise일 수 있어 await가 필요합니다. 클라이언트 상호작용이 필요하면 훅 + Suspense 조합입니다.


레이아웃에 필터를 넣어도 되나요?

레이아웃에 훅을 직접 두면 그 아래 모든 페이지 빌드가 영향받습니다. 레이아웃에는 Suspense 슬롯만 두고, 훅은 자식 클라이언트 컴포넌트에 두세요.


fallback을 null로 두면 안 되나요?

빌드는 통과할 수 있습니다. 쿼리가 채워지기 전 레이아웃이 줄어들어 보입니다. 같은 높이의 자리만 잡아도 덜 흔들립니다.


하이드레이션 에러와 같은 건가요?

아닙니다. 하이드레이션은 서버 HTML과 브라우저 첫 그림이 다를 때 런타임에 납니다. 이 줄은 빌드가 정적 산출물을 못 만들 때 납니다. 고치는 위치도 Suspense 경계와 결정론 코드로 갈립니다.


missing suspense boundary는 쿼리를 읽는 칸에 대체 화면이 없다는 뜻입니다. 빌드 로그의 파일을 열고 그 컴포넌트만 감싸세요. 관련 글: 하이드레이션, 빌드 메모리, 넥스트 에러.


useSearchParamsSuspensemissing boundary넥스트빌드App Router프론트엔드CSR쿼리개발자

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기