Next.js: dynamic() 로드 시 ssr: false 옵션을 활용한 외부 차트 렌더링
- 원인: Node.js 서버에는 브라우저 전용 window/canvas API가 없어서, 차트 라이브러리를 그냥 import하면 사전 렌더링 시점에 에러가 남.
- 해결: Pages Router든 App Router든 →
next/dynamic+ssr: false로 지연 로드. App Router라면 호출 파일에'use client'도 같이 선언. - 주의:
typeof window !== "undefined"조건문으로 우회하는 방식은 임시방편일 뿐, App Router 구조에서는 근본 해결이 아님.
const DynamicChart = dynamic(() => import('react-apexcharts'), { ssr: false });
↓ 왜 이런 에러가 나는지 원리 보기
Next.js 프로젝트에 ApexCharts나 Chart.js 같은 차트 라이브러리를 붙이자마자 로컬 개발 서버가 ReferenceError: window is not defined 혹은 canvas is not defined를 던지는 경우가 흔합니다. 원인은 단순합니다. Next.js는 페이지를 브라우저가 아니라 서버(Node.js) 환경에서 먼저 HTML로 렌더링하는데, 이 서버 런타임에는 애초에 window나 <canvas> 같은 브라우저 전용 객체가 존재하지 않기 때문입니다.
해결책은 next/dynamic의 ssr: false 옵션으로 차트 컴포넌트를 서버 렌더링 대상에서 빼고, 브라우저에 마운트된 뒤에만 불러오는 것입니다.
원인: 서버에는 window도, canvas도 없다
Next.js는 기본적으로 모든 페이지를 서버에서 미리 HTML로 렌더링(Pre-rendering)합니다. 이 과정은 브라우저가 아니라 백엔드인 Node.js 런타임 환경에서 수행됩니다.
반면 ApexCharts나 Chart.js 같은 차트 라이브러리는 내부적으로 브라우저의 window 전역 객체를 참조하고 <canvas> 요소를 직접 조작해서 그래픽을 그립니다. Node.js 서버 환경에는 이 window 객체나 Canvas API가 아예 존재하지 않으므로, 컴포넌트 최상단에서 해당 라이브러리를 그냥 import하면 사전 렌더링 시점에 존재하지도 않는 API를 호출하려다 에러가 나는 것입니다.
렌더링 방식 비교
서버사이드 Pre-rendering 단계에서 차트를 그대로 로드했을 때와, next/dynamic을 적용해 클라이언트에서만 렌더링했을 때의 차이입니다.
일반적인 SSR 임포트 시도
- 에러 발생:
window is not defined로 페이지 전체 로딩이 중단됨. - 서버 부하: 쓰지도 못할 차트 스크립트를 서버가 매번 실행하려 시도함.
- 코드 지저분함:
typeof window !== "undefined"같은 조건부 로직이 계속 늘어남.
next/dynamic (ssr: false) 적용
- 정상 렌더링: 서버 단계에서는 플레이스홀더만 보여주고, 마운트 후 차트를 그림.
- 번들 분리: 무거운 차트 라이브러리가 별도 청크로 나뉘어 필요할 때만 로드됨.
- 선언적 코드: 컴포넌트 선언부에 옵션 하나만 추가하면 됨.
next/dynamic 적용 예제
react-apexcharts를 지연 로드하는 예시입니다. 로딩 중에는 플레이스홀더를 보여줘서 레이아웃이 갑자기 튀는 것도 막아줍니다.
// 1. next/dynamic 라이브러리 임포트
import dynamic from 'next/dynamic';
import React from 'react';
// 2. 외부 차트 라이브러리 컴포넌트를 ssr: false 옵션과 로딩 플레이스홀더를 주어 동적으로 가져옵니다.
const DynamicChart = dynamic(
() => import('react-apexcharts'),
{
ssr: false,
loading: () => (
<div style={{
height: '350px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f8fafc',
borderRadius: '8px',
color: '#64748b',
border: '1px dashed #cbd5e1',
fontSize: '14px'
}}>
차트 데이터를 로딩하고 있습니다...
</div>
)
}
);
// 3. 차트 컴포넌트를 사용하는 메인 페이지 컴포넌트 정의
export default function DashboardPage() {
const chartOptions = {
chart: {
id: 'monthly-sales-chart',
toolbar: { show: false }
},
xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
},
colors: ['#ff5f2e']
};
const chartSeries = [
{
name: 'Sales (KRW)',
data: [30, 40, 45, 50, 49, 60]
}
];
return (
<div style={{ padding: '24px', maxWidth: '800px', margin: '0 auto', fontFamily: 'sans-serif' }}>
<h3 style={{ fontSize: '18px', fontWeight: 'bold', color: '#1e293b', marginBottom: '16px' }}>
월별 매출 현황 분석 대시보드
</h3>
{/* DynamicChart 컴포넌트는 서버에서 배제되고 브라우저 마운트 직후 렌더링됩니다. */}
<div style={{ marginTop: '20px' }}>
<DynamicChart
options={chartOptions}
series={chartSeries}
type="line"
height={350}
/>
</div>
</div>
);
}
헷갈리기 쉬운 지점: App Router에선 'use client'가 먼저다
App Router(app/ 디렉터리) 구조에서 dynamic(..., { ssr: false })를 서버 컴포넌트 파일에 그대로 쓰면 ssr: false is not allowed with next/dynamic in Server Components 에러가 납니다. ssr: false 옵션은 클라이언트 컴포넌트 안에서만 허용되기 때문에, dynamic import를 호출하는 파일 맨 위에 'use client'를 먼저 선언해야 합니다. Pages Router 시절 예제를 그대로 옮기다가 이 지점에서 막히는 경우가 많습니다.
결과적으로 기본 import 방식은 서버가 애초에 처리하지 못할 코드를 렌더링하려다 실패하지만, dynamic(ssr:false)는 차트 라이브러리를 별도 청크로 분리해 브라우저 마운트 이후에만 불러오기 때문에 초기 번들 크기와 로딩 지연이 함께 줄어듭니다.
Next.js dynamic import 스펙과 브라우저 Canvas/window 관련 배경 지식입니다.
정리하면, next/dynamic + ssr: false는 브라우저 전용 라이브러리를 안전하게 불러오는 방법이지만, App Router에서는 'use client' 위치까지 같이 챙겨야 에러가 재발하지 않습니다. 서버/클라이언트 컴포넌트 경계나 Hydration 쪽 이슈를 더 보고 싶다면
[호마다의 IT 개발 입문 블로그]
에도 관련 글을 정리해두었습니다.
댓글
댓글 쓰기