본문으로 건너뛰기
코드룸CODE ROOMTENMO ↗

Next.js 교육 자료, useSearchParams와 Suspense 에러 완벽 해결하기

Next.js App Router에서 쿼리 스트링을 다룰 때 발생하는 대표적인 경고 및 빌드 에러의 원인을 분석하고, Suspense를 활용한 올바른 해결 방법을 학습합니다.

문제 상황 및 에러 원인 분석

  1. Next.js 빌드 시 에러 발생: Entire page deopted into client-side rendering... 이라는 문구와 함께 빌드가 실패하거나 경고가 뜬다.

  2. 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의 강력한 정적 최적화 혜택을 누릴 수 있어 성능이 향상됩니다.

블로그 목록으로