이번 이슈는 `useSearchParams`를 평소처럼 사용했을 뿐인데 빌드가 실패하면서 인지하게 되었습니다. dev 환경에서는 전혀 문제가 없었고 에러 로그를 보고 원인을 파악할 수 있었는데, 찾아보니 Next.js 14.1부터 `useSearchParams`는 공식적으로 `Suspense`로 감싸야 한다는 제약이 생겼고, 15 버전에서는 아예 감싸지 않으면 빌드 자체가 중단되는 수준으로 엄격하게 제한하고 있다는 것을 알게 되었습니다. 이전에는 아무 문제없이 작동하던 코드였기 때문에 바뀐 사용법을 모르고 넘어가기 쉬운 부분이었습니다.
가능하던 것이 권장사항이 되었다가, 어느 순간 필수가 되어버리는 흐름을 직접 겪으면서 최신 스택에서는 문서 업데이트를 꾸준히 확인하는 습관이 중요하다는 걸 다시 한번 느꼈습니다. 앞으로도 이런 사용법 변화에 빠르게 대응할 수 있도록, 프레임워크 버전 업그레이드 이후에는 반드시 공식 마이그레이션 가이드나 breaking changes 목록을 꼼꼼히 확인하는 습관을 가져야겠다고 생각했습니다.
문제 상황 분석하기
⨯ useSearchParams() should be wrapped in a suspense boundary at page "/onboarding/information".
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
Error occurred prerendering page "/onboarding/information"
Export encountered an error on /onboarding/information/page: /onboarding/information, exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null
- Next.js 15 버전에서 `useSearchParams()` 훅을 사용중이였는데 빌드가 실패하는 상황입니다.
- 로컬 개발 환경에서는 정상 작동하지만, 프리렌더링 과정에서 CSR 훅 사용으로 인해 에러가 발생하는 문제가 있었습니다.
- 공식 문서에 따르면 `useSearchParams`는 CSR 전용이며 `<Suspense>`로 감싸야 SSR/SSG와 함께 사용할 수 있다는 제약 존재
왜 Suspense가 필요할까요?
useSearchParams와 같은 훅은 클라이언트에서만 값이 확정되므로, 빌드 타임(SSR/SSG)에서는 값을 알 수 없습니다. Next.js에서는 이런 CSR 훅을 감지하면 Suspense boundary 안에서만 실행되도록 강제합니다. Suspense는 '랜더링을 지연하고 fallback을 보여줄 수 있는 장치'이기 때문에, Next.js가 CSR 훅을 안전하게 처리할 수 있습니다.
원인 파악하기
- useSearchParams, usePathname 같은 훅은 Next.js 14부터 서버 프리렌더링 시 사용할 수 없도록 제한됨
| 13.x | 어디서든 사용 가능 (제약 없음) |
| 14.1 | 경고 발생, Suspense 사용 권장 |
| 15.x | Suspense 필수(사용하지 않으면 빌드 실패) |
- 내부적으로 cache() 최적화가 추가되면서, CSR 전용 훅은 반드시 Suspense로 분리해야 함
- dev 환경에서는 경고 수준이었지만, Next.js 15부터는 아예 빌드가 중단되도록 정책이 강화됨
문제 발생 코드
https://github.com/100-hours-a-week/2-hertz-fe/commit/c40ba1a202b3d41fc43e23b616c5bed00a1bdc86
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
export default function OnboardingInformationPage() {
const searchParams = useSearchParams();
const code = searchParams.get('code');
...
}
무엇이 문제였을까?
- 이 코드는 page.tsx 자체에서 useSearchParams()를 호출하고 있습니다.
- App Router 환경에서 page.tsx는 서버 컴포넌트가 기본값이므로, 빌드 시점(SSR/SSG)에도 실행이 시도됩니다.
- 하지만 useSearchParams는 클라이언트 전용 훅이라 서버 프리렌더링 과정에서는 값을 알 수 없어 에러가 발생합니다.
즉, dev 모드에서는 Next.js가 느슨하게 동작해서 문제없이 실행되지만,
빌드 모드에서는 프리렌더링 단계에서 훅 호출이 감지 → Suspense boundary 필요 → 없으면 빌드 실패로 이어진 상황이었습니다.
해결 방법 - Suspense로 감싸는 방식 사용하기
https://github.com/100-hours-a-week/2-hertz-fe/commit/0cf97b541e4d11f735c13f45971e4ed861e54799
1. 훅을 사용하는 컴포넌트 분리하기
공식 문서에서 권장하는 방식은 useSearchParams를 사용하는 부분을 Wrapper 컴포넌트로 분리하고 Suspense로 감싸는 방법을 적용했습니다.
// src/app/onboarding/information/page.tsx
'use client';
import { Suspense } from 'react';
import OnboardingInformationWrapper from './_components/OnboardingInformationWrapper';
export default function OnboardingInformationPage() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<OnboardingInformationWrapper />
</Suspense>
);
}
2. Wrapper 컴포넌트 내부에서 훅 사용
이제 빌드 타임에는 Suspense로 CSR 훅이 감춰지므로 안전하게 사용할 수 있습니다.
// _components/OnboardingInformationWrapper.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function OnboardingInformationWrapper() {
const searchParams = useSearchParams();
const code = searchParams.get('code');
const state = searchParams.get('state');
// 로그인 처리 로직
}
분리해야 하는 이유 - Suspense는 클라이언트 컴포넌트 레벨에서만 작동한다
그럼 페이지 전체를 감싸면 되는거 아닌가? 싶었지만 여전히 에러가 발생해 찾아보았습니다.
`useSearchParams()`는 클라이언트 사이드 전용 훅이라 build 시점에 사용할 수 없고, `Suspense`로 감싸야만 Next.js가 빌드 타임에 해당 훅이 CSR임을 인식하고 처리할 수 있습니다.
그런데 문제는 Suspense가 한 번에 전체 페이지를 감쌀 수 없다는 점입니다.
useSearchParams()를 Suspense로 감싸기 위해서는 정확히 그 훅이 선언된 컴포넌트를 Suspense로 감싸서 사용해야 합니다.
'👩🏻💻 Develop > TroubleShooting' 카테고리의 다른 글
| [Next.js] 중복 ENUM 값 충돌로 인해 발생한 키워드 매핑 오류 해결하기 (2) | 2025.11.24 |
|---|---|
| [Next.js] Refresh Token 만료로 인한 페이지 무한 새로고침 오류 해결하기 (0) | 2025.09.07 |
| [Next.js] next-pwa와 Turbopack 호환성 문제 해결하기 (0) | 2025.09.05 |
| [Next.js] SSE 브라우저 연결 오류 해결하기 (Nginx) (1) | 2025.09.02 |
| [Next.js] App Router에서 조건부 UI 숨김 시 발생한 hydration 오류 (2) | 2025.08.29 |