본문으로 건너뛰기

다이나믹 임포트로 번들 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로 분리하지 않아도 됩니다.

마치며​

문제를 막연히 추측하기보다는, 번들 크기를 직접 분석해보니 명확한 원인을 파악할 수 있었습니다. 이번 경험을 통해 프론트엔드 개발자는 단순히 서비스를 만드는 것에서 그치지 않고, 항상 더 나은 사용자 경험을 위해 고민해야 함을 느꼈습니다.