문제 상황
Next.js 16(App Router)을 사용하는 프로젝트에서 `searchbar`컴포넌트를 구현하던 중,
URL 쿼리 스트링을 읽기 위해 `useSearchParams()`훅을 사용했다.
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import style from "./serachbar.module.css";
export default function Searchbar() {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState("");
const q = searchParams.get("q");
// 이후 렌더링 ...
}
에디터와 개발 모드(`npm run dev`)에서 에러는 없었지만,
프로덕션 모드에서도 확인하기 위해 빌드(`npm run build`)를 수행하자 다음과 같은 에러가 발생했다.
에러 증상
⨯ useSearchParams() should be wrapped in a suspense boundary at page "/".
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
- 개발 서버에서는 에러가 발생하지 않음
- 빌드 시에 위 문구가 뜨면서 빌드 실패
- suspense boundary로 감싸야 한다는 에러가 출력됨
원인 분석 과정
1. `useSearchParams()`에 대해 알아보기
먼저, `useSearchParams()`에 관한 오류이기 때문에 해당 훅에 대해 알아보는게 먼저라고 생각했다.
물론, 에러 메시지에 참고할 링크도 친절하게 나와있지만 해당 훅부터 공식문서를 참고해보자고 생각했다.
https://nextjs.org/docs/app/api-reference/functions/use-search-params
Functions: useSearchParams | Next.js
API Reference for the useSearchParams hook.
nextjs.org
조금 내리다보면, 'Static Rendering' 부분에 `<SearchBar />` 컴포넌트를 구현한 예제가 있다.
그리고 그 바로 밑에 보면 `<SearchBar />` 컴포넌트를 사용하는 Page 컴포넌트가 있다.
import { Suspense } from 'react'
import SearchBar from './search-bar'
// This component passed as a fallback to the Suspense boundary
// will be rendered in place of the search bar in the initial HTML.
// When the value is available during React hydration the fallback
// will be replaced with the `<SearchBar>` component.
function SearchBarFallback() {
return <>placeholder</>
}
export default function Page() {
return (
<>
<nav>
<Suspense fallback={<SearchBarFallback />}>
<SearchBar />
</Suspense>
</nav>
<h1>Dashboard</h1>
</>
)
}
코드를 보면, `<SearchBar />` 컴포넌트를 `<Suspense>`로 감싸서 구현한 것을 볼 수 있다.
2. Suspense Boundary
에러 문구를 해석해보면 다음과 같다.
`useSearchParams()`는 '/' 페이지에서 suspense boundary로 감싸져 있어야 한다.
위 예제로 suspense boundary를 어떻게 적용해야하는지 알았다.
그런데, suspense boundary란 무엇일까?
https://react.dev/reference/react/Suspense
<Suspense> – React
The library for web and native user interfaces
react.dev
리액트에서 제공하는 기능으로,
자식 요소들이 모두 로딩이 끝날때까지 fallback을 보여주는 컴포넌트이다.
3. 왜 <Suspense>를 사용해야 할까?
const router = useRouter();
const q = searchParams.get("q");
렌더링 중에 search params가 아직 준비되지 않으면,
Next.js는 렌더링을 멈추려 하지만 이를 감싸는 `<Suspense />`가 없어서 에러가 발생하는 것이다.
더 자세히 알아보기 위해서,
기존 에러메시이제 출력된 링크를 참고해보기로 했다.
https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
Missing Suspense boundary with useSearchParams
Using App Router Features available in /app
nextjs.org
들어가자마자 가장 위에 왜 이런 에러가 발생하는지 이유가 나온다.
Reading search parameters through useSearchParams() without a Suspense boundary
will opt the entire page into client-side rendering.
This could cause your page to be blank until the client-side JavaScript has loaded.
해석해보면 다음과 같다.
Suspense boundary 없이 useSearchParams()를 이용해서 search parameter를 읽는것은
모든 페이지를 CSR 방식으로 렌더링하게 한다.
이로 인해서 클라이언트에서 JavaScript가 로딩될때까지 페이지가 비어있을 수 있다.
1) Next.js(App Router)의 기본 렌더링은 "서버가 먼저 그려준다"
App Router에서는 기본적으로 서버 컴포넌트(RSC) 중심으로 렌더링된다.
즉, HTML이 서버에서 먼저 만들어져서 내려오고, 클라이언트 JS는 나중에 로딩된 후, 하이드레이션 된다.
2) 그런데 `useSearchParams()`는 '클라이언트 훅'이고, 경우에 따라 렌더링을 멈출 수 있다.
https://nextjs.org/docs/messages/deopted-into-client-rendering
Entire page deopted into client-side rendering
Using App Router Features available in /app
nextjs.org
공식문서에 따르면, Suspense boundary가 없으면
정적 렌더링시에 모든 페이지가 클라이언트 사이드에서 렌더링된다고 적혀있다.
정적 라우팅의 경우에, `useSearchParams()`훅을 호출하면
가장 가까운 Suspense boundary까지의 트리를 클라이언트 사이드에서 렌더링하게 한다.
Suspense가 없으면 대체 UI를 보여줄 방법이 없기 때문에,
이 상태로 서버가 HTML을 만들다가 중단되면, 사용자에게 보낼 완성된 HTML이 애매해진다.
그래서 Next.js는 해당 페이지는 서버에서 미리 렌더링하지 말고,
클라이언트에서 JS가 로드된 다음에 전부 렌더링, 즉 CSR이 되는것이다.
3) Suspense를 사용하면?
가장 가까운 Suspense Boundary까지만 CSR로 만들기 때문에,
나머지는 서버 SSR, 혹은 정적 렌더링을 유지할 수 있다.
즉, '페이지 전체 CSR'을 막고, 동적인 부분만 격리하는 용도로 사용되는것이다.
해결 방법
`<Searchbar />` 컴포넌트를 Suspense로 감싸서 렌더링한다.
import { ReactNode, Suspense } from 'react';
import Searchbar from '../../components/searchbar';
export default function Layout({ children }: { children: ReactNode }) {
return (
<div>
<Suspense>
<Searchbar />
</Suspense>
{children}
</div>
);
}'Web > Next.js' 카테고리의 다른 글
| Next.js 15 / Image Component (2) | 2025.08.13 |
|---|---|
| Next.js 15 / CSS Modules (1) | 2025.08.13 |
| Next.js 15 / File system conventions - layout.js(ts) (2) | 2025.08.12 |
| Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 2 (1) | 2025.08.11 |