Next.js App Router: 'Dynamic server usage' 정적 빌드 에러 원인과 export const dynamic 지시어 해결법
- 원인: Next.js는 기본적으로 페이지를 정적으로 빌드하려 하는데,
cookies()·headers()·searchParams처럼 요청 시점에만 알 수 있는 값을 빌드 타임에 읽으려 해서 발생 - 해결: 페이지 전체가 동적이면
export const dynamic = 'force-dynamic'/ 클라이언트 컴포넌트의useSearchParams는<Suspense>로 감싸기 - 주의: 단순 UI 인터랙션에 쿼리 스트링을 쓰고 있었다면, 애초에
useState로 바꿔서 빌드 최적화를 방해하지 않게 재설계하는 것도 방법
export const dynamic = 'force-dynamic'; // 이 라우트는 매 요청마다 렌더링
↓ 왜 이런 에러가 나는지 원리 보기
Next.js App Router로 배포 빌드를 실행하다 'Dynamic server usage' 에러를 만나는 경우가 있습니다. 로컬 개발 환경에서는 문제없이 동작하던 페이지가 빌드 단계에서만 실패하는 이유는 요청 시점에만 알 수 있는 값(쿠키, 헤더, 쿼리 등)을 정적 빌드 시점에 미리 읽으려 하기 때문입니다. 원인과 해결 코드, 정적/동적 페이지를 구분하는 기준을 함께 정리합니다.
'Dynamic server usage' 에러의 근본적인 원인
Next.js App Router는 기본적으로 특별한 동적 기능이 감지되지 않으면 모든 페이지를 정적 페이지(Static Route)로 빌드하려고 시도합니다. 정적으로 빌드된 페이지는 서버 부하를 줄이고 CDN을 통해 초고속으로 전송할 수 있기 때문입니다. 하지만 정적 빌드가 진행되는 도중(Build Time), 코드 내부에서 동적 API 또는 함수를 사용하면 빌드 타임에 값을 결정할 수 없으므로 Next.js가 강제로 빌드를 차단하며 Dynamic server usage 에러를 발생시킵니다. 로그인 여부에 따라 다른 내용을 보여주려고 쿠키를 읽는 페이지에서 이 에러를 가장 자주 마주치게 됩니다.
이 에러를 유발하는 대표적인 동적 함수는 네 가지입니다. HTTP 헤더 정보에 접근하는 headers(), 쿠키를 읽고 쓰는 cookies(), 클라이언트에서 쿼리를 읽는 useSearchParams(), 그리고 서버 컴포넌트에서 쿼리에 접근하는 searchParams props가 여기에 해당합니다. 이 함수들은 모두 요청이 들어오는 시점에만 값을 알 수 있기 때문에, 빌드 시점에 미리 정적 HTML을 만들려는 Next.js의 기본 동작과 충돌합니다.
에러를 해결하는 3가지 실전 솔루션
솔루션 1: 해당 서버 컴포넌트를 동적 렌더링(Dynamic Rendering)으로 강제 전환
에러가 발생한 페이지나 레이아웃 파일 최상단에 Route Segment Config인 dynamic 옵션을 선언하여 Next.js 엔진에게 "이 페이지는 빌드 시점이 아니라, 매 요청마다 서버에서 실시간으로 렌더링해야 해"라고 알려주는 방법입니다.
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
// Route Segment Config 추가: 페이지를 Dynamic 렌더링으로 강제 지정
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const cookieStore = await cookies();
const theme = cookieStore.get('theme')?.value || 'light';
return (
<div>
<h2>대시보드</h2>
<p>현재 설정된 테마: {theme}</p>
</div>
);
}
솔루션 2: 클라이언트 컴포넌트 내 useSearchParams를 <Suspense>로 감싸기
클라이언트 컴포넌트("use client") 내부에서 URL 파라미터를 읽기 위해 useSearchParams()를 호출할 때 발생하는 정적 빌드 에러는, 해당 호출부를 <Suspense> 경계로 감싸서 해결합니다.
// app/search/SearchBox.tsx
"use client";
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
// 1. searchParams를 사용하는 컴포넌트 분리
function SearchInput() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
return <div>검색어: {query}</div>;
}
// 2. 부모 컴포넌트에서 Suspense로 래핑하여 Export
export default function SearchBox() {
return (
<Suspense fallback={<div>검색창 로딩 중...</div>}>
<SearchInput />
</Suspense>
);
}
솔루션 3: 동적 데이터가 불필요한 영역은 렌더링 분리
단순히 특정 탭 전환이나 UI 인터랙션을 위해 쿼리 스트링을 남발하고 있었다면, searchParams 대신 React State(useState)나 클라이언트 사이드 Router를 활용하여 해당 컴포넌트가 빌드 최적화 흐름을 방해하지 않도록 구조를 재설계해야 합니다.
정적으로 배포하려던 소개 페이지 하나에 로그인 여부만 확인하려고 cookies()를 가볍게 가져다 썼다가, 페이지 전체가 정적 빌드에서 빠지는 걸 뒤늦게 알아챈 적이 있습니다. 로그인 배지 하나 때문에 페이지 전체가 매 요청마다 서버 렌더링되고 있었던 셈이라, 그 부분만 클라이언트 컴포넌트로 떼어내고 나서야 나머지는 다시 정적으로 빌드됐습니다.
요약: Next.js Static / Dynamic 구분 기준
Next.js App Router가 최적화를 위해 페이지를 분류하는 기준입니다.
| 구분 항목 | 정적 렌더링 (Static Rendering) | 동적 렌더링 (Dynamic Rendering) |
|---|---|---|
| 렌더링 시점 | 빌드 시점(Build Time) 또는 백그라운드 재검증 시 | 매 요청 시점(Request Time) 실시간 서버 연산 |
| 트리거 조건 | 기본 설정 상태, 캐싱된 데이터 요청 | headers(), cookies(), searchParams 사용 시 |
| 추천 대상 | 공통 소개 페이지, 일반 블로그 포스트 | 개인 대시보드, 마이페이지, 세션 체크 영역 |
Next.js 공식 문서의 렌더링 최적화 규칙과 빌드 옵션 Segment 명세는 아래 문서에서 확인할 수 있습니다.
정적/동적 렌더링 중 어느 쪽이 맞을지 애매하다면, 페이지 전체보다 일부 컴포넌트만 동적으로 분리하는 것도 방법입니다. 캐싱과 렌더링 전략 자체가 낯설다면 [호마다의 IT 개발 입문 블로그] 도 같이 보셔도 좋습니다.
댓글
댓글 쓰기