TechFeedTechFeed
Frontend

useSearchParams, Suspense, 정적 빌드 | 로컬은 되는데 빌드만 멈추면?

정적 페이지에서 useSearchParams를 쓰면 가장 가까운 서스펜스 경계까지가 클라이언트에서만 그려집니다. 개발 서버는 통과하고 next build만 missing suspense로 멈춥니다. 훅 자식만 감싸거나 카카오 code는 서버 searchParams로 읽습니다. 넥스트, 리액트, 프론트엔드, 앱 라우터, 1인 개발자 기준. 2026년 9월 Next.js 공식 문서.

by

핵심만: 정적 페이지에서 useSearchParams를 쓰면, 가장 가까운 서스펜스 경계까지가 클라이언트에서만 그려집니다. 경계를 빼면 개발은 넘어가고, 배포 빌드만 멈춥니다.


카카오 콜백을 클라이언트 페이지로 두고 code만 읽으니, next dev는 조용하고 next build만 빨간 줄이었습니다. 서버 페이지의 searchParams 프로미스, 훅 지시자, window, 객체 자식과는 자리를 나눕니다.


이런 분에게
  • 로컬은 되는데 배포 빌드만 멈추는 사람
  • 카카오 code를 클라이언트 훅으로 읽은 사람
  • 필터 칩 때문에 페이지 전체가 하얘진 사람

※ 기준일 2026-09-04. 넥스트 useSearchParams, 서스펜스 경계, 카카오 인가 코드.


로컬은 되는데 빌드만 죽는 이유

개발은 요청마다 그리니 서스펜스가 없어도 통과합니다. 정적 빌드는 쿼리를 모르는 채로 HTML을 미리 만들어야 해서, 훅을 감싼 경계가 없으면 그 자리에서 멈춥니다.


넥스트 useSearchParams 문서가 이 분기를 분명히 적습니다. 개발 모드에서는 경로를 그때그때 그리므로 훅이 바로 값을 줍니다. 프로덕션 빌드에서 정적 페이지가 같은 훅을 호출하면, 서스펜스 경계가 없을 때 Missing Suspense boundary with useSearchParams 메시지가 납니다.


메시지 페이지가 말하는 문제는 화면이 잠깐 비는 일입니다. 경계를 빼면 그 페이지가 통째로 클라이언트에서만 그려지고, 자바스크립트가 내려오기 전까지 HTML이 비어 보일 수 있습니다. 그래서 빌드가 그냥 통과시키지 않습니다.


슬러그를 동기로 꺼낸 경고와 헷갈리지 마세요. 그건 페이지 props가 프로미스인 칸입니다. 여기는 클라이언트 훅이 정적 껍데기를 깨는 칸입니다. 훅 지시자를 안 붙여서 난 빨간 줄, window를 맨 위에서 읽어서 난 줄, 객체를 화면 자식으로 넣어서 난 줄과도 고치는 자리가 다릅니다.


먼저 기억할 것 | next dev가 초록이어도 끝난 게 아닙니다. 정적 페이지에서 이 훅을 썼다면 next build를 한 번 돌려 보세요. 빨간 줄의 파일 이름이 훅을 호출한 클라이언트 컴포넌트입니다.


훅을 페이지 통째에 두면 정적 껍데기가 사라진다

경계가 없으면 그 페이지 HTML이 통째로 비어 나갑니다. 훅은 클라이언트 전용이라 서버 컴포넌트에서는 쓸 수 없고, 호출한 트리 위쪽으로 가장 가까운 서스펜스까지가 클라이언트 렌더로 빠집니다.


증상원인먼저 할 일
next dev는 되고 next build만 에러정적 페이지에서 훅을 경계 없이 호출훅 쓰는 자식만 서스펜스로 감싸기
배포 첫 화면이 하얗다가 늦게 뜸페이지 전체가 클라이언트 렌더로 빠짐경계를 훅 바로 위에 두기
필터 칩 하나 때문에 레이아웃까지 비함훅이 페이지 파일에 붙어 있음칩만 클라이언트 자식으로 쪼개기
어느 파일인지 스택이 안 보임빌드가 압축된 이름만 남김next build --debug-prerender
페이지 파일에 force-dynamic을 넣었는데 그대로클라이언트 페이지의 그 줄은 효과가 없음서버 페이지에서 connection을 기다리기

어느 컴포넌트인지 안 보이면 next build --debug-prerender를 씁니다. 공식 메시지 페이지가 이 플래그로 압축 안 된 스택을 보라고 적습니다. 1인 쇼핑몰 상품 목록에 정렬 쿼리 하나만 붙였을 때도, 페이지 파일에 훅이 있으면 목록 HTML 전체가 빠집니다.


빌드를 죽이는 클라이언트 페이지
'use client'; import { useSearchParams } from 'next/navigation'; // 정적 페이지에서 이 파일 전체가 클라이언트 렌더로 빠진다 export default function KakaoCallbackPage() { const searchParams = useSearchParams(); const code = searchParams.get('code'); return <p>코드: {code}</p>; }
넥스트 정적 빌드가 useSearchParams 서스펜스 경계 없음으로 멈추는 개발 모니터
개발 서버는 넘어가고, 정적 빌드만 missing suspense로 멈춘다

가장 작은 자식만 서스펜스로 감싼다

훅을 쓰는 컴포넌트만 서스펜스로 감싸면 껍데기는 남습니다. 제목과 본문은 미리 그려 두고, 쿼리를 읽는 칩만 나중에 채우면 됩니다.


공식 훅 문서 예제도 검색창만 감쌉니다. 페이지는 서버 컴포넌트로 두고, 훅은 클라이언트 자식에만 둡니다. 폴백은 같은 높이의 빈 칸이면 레이아웃이 덜 흔들립니다.


서버 페이지가 이미 searchParams 프로미스를 받고 있다면, 그 프로미스를 클라이언트 자식에 통째로 넘긴 뒤 use로 풀 수도 있습니다. 메시지 페이지가 이 패턴도 서스펜스가 필요하다고 적습니다. 훅을 쓰든 프로미스를 풀든, 쿼리를 읽는 칸 위에는 경계가 있어야 합니다.


페이지 파일 맨 위에 클라이언트 지시자를 붙여 통째로 훅을 쓰는 방식은, 버튼 상태 때문에 지시자가 필요했던 칸과 섞이지 마세요. 지시자는 훅 규칙이고, 서스펜스는 정적 껍데기 칸입니다.


훅 쓰는 자식만 감싸기
import { Suspense } from 'react'; import CodeBadge from './code-badge'; function CodeFallback() { return <p>코드를 읽는 중</p>; } export default function Page() { return ( <> <h1>로그인 콜백</h1> <Suspense fallback={<CodeFallback />}> <CodeBadge /> </Suspense> </> ); } // code-badge.js 'use client'; import { useSearchParams } from 'next/navigation'; export default function CodeBadge() { const code = useSearchParams().get('code'); return <p>{code ? '코드가 있습니다' : '코드가 없습니다'}</p>; }

카카오 콜백은 서버 페이지에서 code를 기다린다

인가 코드는 클라이언트 훅보다 페이지 쿼리 칸이 맞습니다. 토큰 교환은 서버에서 하는 일이라, 콜백 페이지를 클라이언트 파일로 바꿀 이유가 거의 없습니다.


카카오 인가 코드 요청이 돌려 주는 주소는 redirect_uri?code=... 형태입니다. 서버 페이지에서 await searchParams로 문자열을 받은 뒤 교환하면, 이 훅과 서스펜스 경계를 콜백에 들일 필요가 없습니다. 토스 결제 성공 주소의 주문번호도 같은 자리입니다.


스피너나 실패 문구만 클라이언트여야 한다면, 코드 문자열은 서버가 읽고 자식에는 글자만 넘기세요. 쿼리 객체 전체를 화면에 넣으면 객체 자식 에러가 납니다. 그건 다른 칸입니다.


제가 콜백을 클라이언트 페이지로 옮긴 이유는 로딩 문구를 바꾸고 싶어서였습니다. 그 한 줄 때문에 빌드가 죽었습니다. 로딩 문구는 폴백만 있으면 되고, 코드 자체는 서버가 읽는 편이 짧습니다.


카카오 콜백은 서버에서 code를 기다린다
// app/auth/kakao/callback/page.js export default async function KakaoCallback({ searchParams }) { const { code, error } = await searchParams; if (error || !code || Array.isArray(code)) { return <p>로그인 코드가 없습니다.</p>; } await exchangeCode(code); return <p>로그인 처리 중</p>; }
카카오 로그인 콜백 주소의 code 쿼리를 서버 페이지에서 읽는 작업 화면
인가 코드는 훅이 아니라 서버 페이지 searchParams 칸이다

처음부터 동적이면 connection을 먼저 기다린다

요청이 온 뒤에만 그리려면 서버에서 connection을 기다립니다. 훅 문서가 정적 껍데기를 포기할 때는 이 함수를 페이지나 레이아웃에서 먼저 기다리라고 적습니다.


connection은 들어오는 요청을 기다린 뒤에 아래를 그립니다. 예전에 쓰던 export const dynamic = 'force-dynamic'보다 요청에 묶인 뜻이 분명합니다. 그 줄을 클라이언트 페이지에 넣으면 효과가 없습니다. 메시지 페이지가 이 함정을 따로 적습니다.


상품 목록처럼 제목은 바로 보여주고 정렬만 쿼리에 맡길 때는 자식을 감싸는 쪽이 맞습니다. 로그인 직후처럼 요청마다 다른 화면이면 서버 페이지에서 기다린 뒤 훅을 써도 됩니다. 정적 껍데기를 남길지, 처음부터 동적으로 갈지 먼저 고르세요.


loading 파일과 설정으로 끄기는 다른 칸

loading 파일은 페이지 전체를 감싸고, 끄기 옵션은 14에만 있습니다. 경계를 어디에 두느냐가 남기는 HTML 크기를 가릅니다.


방법남는 것쓸 때
훅 쓰는 자식만 Suspense제목·본문 정적 HTML필터, 정렬, 공유 주소
서버 searchParams를 기다림훅 자체가 없음카카오 code, 토스 주문번호
connection을 먼저 기다림그 아래는 요청마다 그림처음부터 동적 화면
loading.js세그먼트 전체가 경계페이지 단위 로딩만 필요할 때
missingSuspenseWithCSRBailout false경고만 끔, 14 전용쓰지 말 것. 이후 메이저에서 제거

loading.js는 그 폴더 세그먼트를 서스펜스로 감쌉니다. 빌드 에러는 사라질 수 있어도, 첫 HTML에서 페이지 본문까지 폴백으로 빠질 수 있습니다. 필터 칩 하나 때문에 본문 전체를 비울 필요는 없습니다.


넥스트 14에는 missingSuspenseWithCSRBailout: false로 검사를 끄는 실험 옵션이 있었습니다. 메시지 페이지가 14에서만 되고, 이후 버전에서는 고치라고 적습니다. 설정을 꺼서 빌드만 통과시키면 사용자는 빈 화면을 먼저 봅니다.


값을 읽는 방식은 URLSearchParams와 같습니다. get은 첫 값, 같은 키가 두 번이면 getAll입니다. 카카오 code는 키가 하나라 문자열일 때만 토큰 교환으로 넘기면 됩니다.


서스펜스 경계와 loading 파일 위치를 비교하는 모니터 책상
훅 바로 위를 감싸면 껍데기가 남고, loading 파일은 세그먼트 전체를 감싼다

끄기 설정은 고친 게 아니다 | 14의 실험 옵션은 빈 첫 화면을 그대로 둡니다. 훅을 자식으로 옮기거나, 서버에서 쿼리를 기다리세요. 하이드레이션 경고를 숨기는 속성과도 칸이 다릅니다.


참고 자료


자주 묻는 질문

next dev는 되는데 빌드만 죽어요.

개발은 요청마다 그려서 훅이 바로 값을 줍니다. 정적 빌드는 쿼리를 모르니 서스펜스 경계가 없으면 멈춥니다. 훅 쓰는 자식만 감싸거나, 서버 페이지에서 쿼리를 기다리세요.


페이지에 force-dynamic을 넣었는데 그대로예요.

클라이언트 페이지의 그 줄은 효과가 없습니다. 서버 페이지나 레이아웃에서 connection을 기다리세요. 정적 껍데기를 남길 거면 훅 자식만 감싸는 쪽이 맞습니다.


카카오 code를 훅으로 읽어도 되나요.

토큰 교환은 서버 일입니다. 콜백은 서버 페이지에서 searchParams를 기다리는 편이 짧습니다. 로딩 문구만 필요하면 폴백을 두고, 코드 문자열은 서버가 읽으세요.


loading.js만 넣으면 끝나나요.

세그먼트 전체가 경계가 되어 빌드는 통과할 수 있습니다. 본문 HTML까지 폴백으로 빠질 수 있으니, 필터 칩이라면 그 자식만 감싸는 쪽이 남기는 화면이 큽니다.


설정을 끄면 안 되나요.

missingSuspenseWithCSRBailout false는 넥스트 14 실험 옵션이고, 이후 메이저에서 빠집니다. 검사를 끄면 사용자는 빈 첫 화면을 봅니다. 경계를 두거나 서버에서 읽으세요.


params 프로미스 경고랑 같은 건가요.

아닙니다. 프로미스 칸은 서버 페이지 props를 동기로 읽어서 납니다. 여기는 클라이언트 훅이 정적 껍데기를 깨는 칸입니다. 훅 지시자, window, 객체 자식, 하이드레이션과도 자리를 나눕니다.


useSearchParamsSuspense정적 빌드missing suspenseNext.jsReact앱 라우터카카오 로그인프론트엔드CSR bailoutloading.js개발자

관련 도구

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기