Next.js App Router에서 쿼리 스트링을 다룰 때 발생하는 대표적인 경고 및 빌드 에러의 원인을 분석하고, Suspense를 활용한 올바른 해결 방법을 학습합니다.
문제 상황 및 에러 원인 분석
-
Next.js 빌드 시 에러 발생:
Entire page deopted into client-side rendering...이라는 문구와 함께 빌드가 실패하거나 경고가 뜬다. -
Dynamic Server Usage 에러: 배포 후 런타임에 페이지가 정상적으로 정적 최적화되지 않고 서버 부하를 유도한다.
왜 이런 일이 발생할까?
Next.js는 빌드 시점에 페이지들을 최대한 미리 HTML로 만들어두는 정적 생성을 시도합니다.
하지만 useSearchParams는 사용자가 브라우저에서 접속했을 때의 URL 쿼리 스트링을 읽어오는 훅입니다. 빌드 시점에는 사용자가 어떤 쿼리 스트링(?search=abc 등)을 들고 들어올지 알 수 없습니다.
따라서 useSearchParams가 사용된 컴포넌트를 만나면 Next.js는 다음과 같이 판단합니다.
"어? 이 컴포넌트는 미리 static하게 만들 수가 없네? 이 컴포넌트가 속한 페이지 전체를 static에서 제외하고, 클라이언트가 요청할 때마다 새로 그리거나(SSR) 아예 브라우저에서 그리도록(CSR) 바꿔야겠다."
이를 방지하기 위해 경고나 에러를 발생시킵니다.
해결 열쇠, Suspense와 Boundary 분리
이 문제를 해결하는 핵심은 "값이 준비되지 않았을 때 보여줄 Fallback를 지정하고, 영향 범위를 최소한으로 격리하는 것"입니다. React의 Suspense가 바로 이 역할을 합니다.
useSearchParams를 사용하는 비즈니스 로직을 별도의 서브 컴포넌트로 분리하고, 이를 Suspense로 감싸주면 Next.js는 Suspense 내부만 클라이언트 사이드에서 처리하고, 나머지 페이지 영역은 여전히 정적(Static)으로 빌드할 수 있게 됩니다.
올바른 리팩토링 단계
잘못된 예시 (페이지 전체가 정적 최적화에서 탈락함)
// app/search/page.tsx
'use client';
import { useSearchParams } from 'next/navigation';
export default function SearchPage() {
// 🚨 페이지 루트 수준에서 바로 호출하여 페이지 전체가 Deopt 됨
const searchParams = useSearchParams();
const query = searchParams.get('q');
return (
<div className="p-8">
<header className="border-b pb-4 mb-4">
<h1 className="text-2xl font-bold">검색 결과 페이지</h1>
</header>
<main>
<p>입력하신 검색어: <strong>{query}</strong></p>
</main>
</div>
);
}
올바른 예시 (Suspense 활용)
단계 1: useSearchParams를 사용하는 핵심 로직만 별도 컴포넌트로 추출합니다. 단계 2: 메인 페이지 컴포넌트에서는 해당 컴포넌트를 <Suspense>로 감싸서 렌더링합니다.
// app/search/page.tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
// 1️⃣ 쿼리를 처리하는 컴포넌트를 별도로 분리
function SearchResult() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return (
<p>입력하신 검색어: <strong>{query || '없음'}</strong></p>
);
}
// 2️⃣ 메인 페이지 컴포넌트 (정적 최적화 유지 가능)
export default function SearchPage() {
return (
<div className="p-8">
<header className="border-b pb-4 mb-4">
<h1 className="text-2xl font-bold">검색 결과 페이지</h1>
</header>
<main>
{/* 3️⃣ useSearchParams를 쓰는 컴포넌트를 Suspense Boundary로 격리 */}
<Suspense fallback={<p>검색 결과를 불러오는 중입니다...</p>
}>
<SearchResult />
</Suspense>
</main>
</div>
);
}
핵심 정리
-
useSearchParams는 런타임 값이다: 빌드 시점에는 알 수 없는 데이터이므로 정적 빌드를 방해합니다. -
Suspense로 경계를 나누자:useSearchParams를 사용하는 컴포넌트를 가볍게 만들고 반드시<Suspense fallback={...}>으로 감싸야 합니다. -
이점: 이렇게 경계를 나누면 검색창이나 결과 메시지 외에 페이지의 다른 레이아웃(헤더, 푸터, 사이드바 등)은 여전히 Next.js의 강력한 정적 최적화 혜택을 누릴 수 있어 성능이 향상됩니다.