← 프로젝트 회고 · Next/Express
RETROSPECTIVE · 회고

Next.js 정적 export에서 useSearchParams가 페이지를 비워버리는 이유

Next/Express

개인공부 목록 페이지를 output: "export"로 정적 생성했는데, 빌드된 HTML을 열어보니 글 링크가 하나도 없었다. 브라우저에서는 멀쩡히 보이는데 크롤러가 받는 HTML에는 목록이 통째로 빠져 있었다.

원인: CSR bailout

카테고리를 URL로 유지하려고 ?category=Java를 useSearchParams()로 읽고 있었다. 쿼리스트링은 요청마다 달라지는 값이라 빌드 시점에는 알 수 없다. 그래서 Next.js는 정적 렌더링 중 useSearchParams를 만나면 가장 가까운 Suspense 경계까지를 서버에서 그리지 않고 클라이언트 렌더링으로 넘긴다(client-side rendering bailout).

  1. Suspense로 감싸지 않으면 production 빌드에서 에러가 난다.
  2. Suspense로 감싸면 빌드는 되지만, 경계 안쪽은 HTML에 fallback만 남는다.

빌드 결과물을 검색해보면 BAILOUT_TO_CLIENT_SIDE_RENDERING 표시가 남아 있는 걸 확인할 수 있다.

해결: 초기 렌더에서는 쿼리를 읽지 않는다

SEO가 필요한 영역은 쿼리와 무관하게 전부 렌더링하고, 선택 상태만 마운트 후에 반영하도록 바꿨다.

const [selected, setSelected] = useState(CATEGORIES[0]);

useEffect(() => {
// 정적 HTML에는 기본값으로 그려지고, 브라우저에서만 URL 값을 반영
const fromUrl = new URLSearchParams(window.location.search).get("category");
if (isCategory(fromUrl)) setSelected(fromUrl);
}, []);

return CATEGORIES.map((cat) => (
// 모든 카테고리 목록을 HTML에 남기고 선택된 것만 보이게
<section key={cat} hidden={cat !== selected}>…</section>
));

이렇게 하면 빌드된 HTML에 모든 글 링크가 남고, 사용자는 기존처럼 카테고리를 전환할 수 있다.

정리

  1. 정적 export에서 useSearchParams는 사실상 "이 영역은 클라이언트에서 그린다"는 선언이다.
  2. 검색엔진에 보여야 하는 콘텐츠는 요청 시점 값(쿼리, 쿠키 등)에 의존하지 않게 렌더링한다.
  3. 눈으로 확인하는 것만으로는 모른다. 빌드 결과 HTML을 직접 열어보는 습관이 필요하다.
Gunmo Lee
Next.js 정적 export에서 useSearchParams가 페이지를 비워버리는 이유 | Gunmo's Dev Life