본문으로 건너뛰기

다이나믹 임포트로 번들 400KB로 줄이기

· 약 4분
김현재
개발자

들어가며

사내 보안 이벤트 트래킹 서비스를 개발하면서 성능 이슈를 마주했습니다. 서비스 특성상 여러 종류의 차트를 활용해야 했고, 점점 차트가 많아질수록 접속 속도가 눈에 띄게 느려졌습니다. 그 중 특히 이상했던 점은, 기능이 추가되며 전체 서비스 크기는 커지는데 로그인 페이지조차 점차 느려진다는 것이었습니다. 로그인 화면은 변화가 없었기 때문에 더욱 의아했습니다.

이 문제가 왜 발생했는지, 그리고 어떤 방식으로 해결했는지 이번 포스팅에 차근차근 정리해보았습니다.

본 포스팅은 사내 프로젝트의 경험을 바탕으로 작성되었으나, 보안 정책상 실제 코드와 이미지를 그대로 사용할 수 없습니다. 따라서 프로젝트에서 해결한 핵심 로직과 메커니즘을 동일하게 재현한 별도의 데모 코드와 자료를 활용하여 정리했습니다.

문제 파악

원인을 찾기 위해 rollup-plugin-visualizer로 번들 구성을 확인했습니다. 트리맵을 확인해보자 전체 번들의 절반 이상을 차지하는 거대한 블록이 있었는데, 바로 apexcharts 였습니다.

문제는 용량이 아주 큰 차트 라이브러리가 서비스 진입 시점에 항상 다운로드된다는 것이었습니다. 예를 들어 로그인 페이지, 설정 페이지 등 차트가 필요 없는 페이지에서도 모두 차트를 로드하고 있었습니다.

// routes/index.tsx — 기존 코드
import DashboardPage from '../pages/DashboardPage';
import HxPage from '../pages/HxPage';
import ExPage from '../pages/ExPage';

// DashboardPage.tsx — 기존 코드
import ReactApexChart from 'react-apexcharts';

DashboardPage 컴포넌트가 차트 출력을 위해 차트 라이브러리를 import하는데, 라우터에서 모든 페이지를 한 번에 import하다 보니 사용자는 어떤 페이지를 들어가도 전체 코드가 미리 다 같이 다운로드되는 구조였습니다.

그렇다면 어떻게 최적화할 수 있을까요? 기존에는 모든 코드를 하나의 큰 번들 파일로 묶어서, 실제로 사용하지 않는 코드까지 한 번에 다운로드했습니다. 하지만 실제로는 즉시 필요하지 않은 파일들은 처음부터 모두 받을 필요가 없습니다. 여러 파일로 쪼개어, 각 파일이 정말 필요한 순간에만 개별적으로 받아올 수 있게 구조를 변경해야 합니다.

Dynamic Import란?

React에서는 React.lazy()Suspense를 사용해 컴포넌트를 동적으로 임포트할 수 있습니다. Vite(Rollup)는 lazy(() => import(...)) 구문을 만나면 해당 모듈을 별도의 청크 파일로 분리하고, 실제로 해당 컴포넌트가 렌더링될 때 비로소 네트워크 요청을 보냅니다.

Suspense는 청크가 로드되는 동안 보여줄 fallback UI를 지정하기 위해 함께 사용합니다.

**청크(Chunk)**란?

청크는 번들러가 코드를 여러 파일로 나누어 빌드할 때 만들어지는 "작게 쪼개진 JS 파일"입니다.
예를 들어 다이나믹 임포트를 사용하면, 해당 컴포넌트나 라이브러리가 별도의 청크 파일로 분리돼 필요할 때만 개별적으로 네트워크를 통해 다운로드됩니다.

차트 컴포넌트 분리

먼저 차트 관련 컴포넌트를 별도 파일로 분리합니다. 이 파일들이 동적 임포트의 단위가 됩니다.

// components/charts/HxBarChart.tsx
import ReactApexChart from 'react-apexcharts';
import type { ApexOptions } from 'apexcharts';

const options: ApexOptions = {
chart: { type: 'line', toolbar: { show: false } },
colors: ['#6366f1'],
// ...
};

export default function HxBarChart({ data }: { data: number[] }) {
return <ReactApexChart options={options} series={[{ name: '이벤트 추이', data }]} type="line" height={300} />;
}

기존 정적 임포트

import DashboardPage from '../pages/DashboardPage';
import HxPage from '../pages/HxPage';
import ExPage from '../pages/ExPage';

export default function Router() {
return (
<div>
<DashboardPage />
<HxPage />
<ExPage />
</div>
);
}

다이나믹 임포트 적용

import { lazy, Suspense } from 'react';

const DashboardPage = lazy(() => import('../pages/DashboardPage'));
const HxPage = lazy(() => import('../pages/HxPage'));
const ExPage = lazy(() => import('../pages/ExPage'));

function PageSkeleton() {
return (
<div
style={{
height: 300,
background: '#f1f5f9',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
로딩 중...
</div>
);
}

export default function Router() {
return (
<div>
<Suspense fallback={<PageSkeleton />}>
<DashboardPage />
</Suspense>

<Suspense fallback={<PageSkeleton />}>
<HxPage />
</Suspense>

<Suspense fallback={<PageSkeleton />}>
<ExPage />
</Suspense>
</div>
);
}

실제 빌드 결과 비교

정적 임포트

dist/assets/
└── index-[hash].js 916 KB (gzip: 260 KB) ← 전체가 하나의 파일

사용자가 어느 페이지에 접속하든 apexcharts를 포함한 전체 코드가 한 번에 다운로드됩니다.

다이나믹 임포트

dist/assets/
├── index-[hash].js 400 KB (gzip: 130 KB) ← 초기 번들
├── react-apexcharts.min-[hash].js 516 KB (gzip: 135 KB) ← 차트 진입 시 로드
├── DashboardPage-[hash].js 4.56 KB

초기 번들이 400KB로 줄었습니다. apexcharts는 대시보드 페이지에 처음 진입하는 시점에 별도로 로드됩니다. 로그인, 설정 등 차트가 없는 페이지에서는 다운로드 자체가 일어나지 않습니다.

성능 지표 비교

지표BeforeAfter
초기 번들 크기916 KB (gzip: 260 KB)400 KB (gzip: 130 KB)
초기 번들 감소율56% 감소
차트 페이지 진입 시이미 포함+516 KB 추가 로드
비 차트 페이지apexcharts 포함apexcharts 없음

실제로 전송되는 전체 데이터 양은 동일하지만, 초기 진입 속도가 크게 달라집니다.

주의할 점

너무 잘게 쪼개지 않기

모든 컴포넌트에 lazy 로딩을 적용하면 네트워크 요청이 불필요하게 많이 발생할 수 있습니다. 이는 오히려 리소스 낭비로 이어질 수 있으니, 꼭 필요한 컴포넌트에만 동적으로 임포트하는 것이 좋습니다.

첫 방문 시 로딩 시간 증가

초기 번들이 작아지는 대신, 해당 페이지에 처음 진입할 때는 청크를 다운로드하는 시간이 추가됩니다. 사용자가 항상 방문하는 핵심 페이지라면 lazy 적용 여부를 신중하게 판단해야 합니다.

언제 Dynamic Import를 쓸까?
  • 번들 크기가 큰 외부 라이브러리를 사용하는 경우 (차트, 에디터, PDF 등)
  • 특정 조건에서만 보이는 모달, 드로어 같은 UI
  • 어드민, 설정, 마이페이지처럼 접속 빈도가 낮은 페이지
  • 초기 화면과 무관한 기능 영역

반면 로그인 후 항상 보이는 네비게이션, 핵심 레이아웃 컴포넌트는 굳이 lazy로 분리하지 않아도 됩니다.

마치며

단순히 감에 의존해 문제를 추측하기보단, 실제로 번들 크기를 분석하면서 명확한 원인을 찾아낼 수 있었습니다. 프론트엔드 개발자라면 서비스 제공을 넘어 최적의 사용자 경험을 끊임없이 고민해야 합니다.