
초기 로딩 속도 0.5초 빨라지면 매출이 10% 오른다 (번들 사이즈 다이어트)
기능 추가할 때마다 늘어나는 번들 사이즈, 그대로 두면 사용자는 떠납니다. Code Splitting, Tree Shaking, Dynamic Import로 JS 다이어트하는 실제 기법.

기능 추가할 때마다 늘어나는 번들 사이즈, 그대로 두면 사용자는 떠납니다. Code Splitting, Tree Shaking, Dynamic Import로 JS 다이어트하는 실제 기법.
매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

느리다고 느껴서 감으로 최적화했는데 오히려 더 느려졌다. 프로파일러로 병목을 정확히 찾는 법을 배운 이야기.

엄청난 데이터를 아주 적은 메모리로 검사하는 방법. 100% 정확도를 포기하고 99.9%의 효율을 얻는 확률적 자료구조의 세계. 비트코인 지갑과 스팸 필터는 왜 이것을 쓸까요?

텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.

스타트업 초기에는 '기능 구현'이 최우선입니다.
"차트 기능 넣어주세요!", "에디터 기능 넣어주세요!", "3D 뷰어 넣어주세요!"
개발자는 열심히 npm install heavy-library를 입력합니다.
어느 날, 사용자들이 불만을 터뜨립니다. "사이트 들어가는 데 5초나 걸려요. 답답해서 못 쓰겠어요."
Lighthouse 점수를 찍어보니 Performance 30점. 범인은 거대해진 JS 번들 파일(main.js, 5MB)이었습니다. 사용자가 우리 사이트의 '로그인 페이지'만 보려고 해도, 5MB짜리 거대한 자바스크립트 덩어리를 전부 다운로드하고 파싱해야 했던 겁니다.
저는 "요즘 인터넷 빠른데 5MB 정도는 금방 받지 않나?"라고 안일하게 생각했습니다.
하지만 문제는 네트워크 속도(Download Time)만이 아니었습니다.
브라우저가 다운로드한 JS 코드를 해석하고 실행하는 시간(Execution Time)이 더 큰 문제였습니다.
특히 저사양 모바일 기기에서는 5MB를 파싱하는 데만 몇 초가 걸리고, 그동안 화면은 멈춥니다(TBT: Total Blocking Time).
과거의 MPA(Multi Page Application)는 페이지별로 필요한 HTML/JS만 받았습니다.
하지만 React 같은 SPA(Single Page Application)는 기본적으로 모든 페이지의 코드를 하나의 bundle.js로 뭉쳐서(Bundling) 보냅니다.
안 쓰는 관리자 페이지 코드, 결제 모듈 코드가 메인 페이지 로딩을 방해하는 꼴입니다.
이걸 "여행 가방 싸기"에 비유하니 이해가 됐습니다.
우리는 사용자가 지금 당장 필요한 코드만 쪼개서(Splitting) 보내고, 나머지는 필요할 때 가져와야(Lazy Loading) 합니다.
webpack-bundle-analyzer나 rollup-plugin-visualizer로 분석해 보니, 범인은 명확했습니다.
가장 쉽고 효과가 큰 방법입니다. 페이지(Route) 별로 JS 파일을 쪼갭니다.
사용자가 /dashboard에 안 들어가면, 대시보드 코드는 평생 다운로드하지 않습니다.
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 1. 일반 import 대신 lazy import 사용
// import Dashboard from './pages/Dashboard'; (X)
const Dashboard = lazy(() => import('./pages/Dashboard')); // (O)
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
{/* 2. 로딩 중 보여줄 UI (Suspense) 필수 */}
<Suspense fallback={<div>페이지 로딩 중...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
이것만 적용해서 메인 번들 사이즈를 40% 줄였습니다.
페이지뿐만 아니라, 특정 조건에서만 뜨는 무거운 모달이나 컴포넌트도 쪼갤 수 있습니다.
import { useState, lazy, Suspense } from 'react';
// 무거운 텍스트 에디터는 버튼을 눌렀을 때만 로드한다!
const HeavyTextEditor = lazy(() => import('./components/HeavyTextEditor'));
function WritePage() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<button onClick={() => setShowEditor(true)}>글쓰기</button>
{showEditor && (
<Suspense fallback={<div>에디터 불러오는 중...</div>}>
<HeavyTextEditor />
</Suspense>
)}
</div>
);
}
이렇게 하면 초기 페이지 진입 시에는 에디터 라이브러리 용량을 전혀 차지하지 않습니다.
라이브러리 전체를 불러오지 말고, 필요한 함수만 불러와야 합니다.
/* 나쁜 예 (CommonJS, 전체 로드) */
// const _ = require('lodash');
// _.debounce(...) // Lodash의 모든 함수를 다 번들에 포함시킴 (약 70KB)
/* 좋은 예 (ES Modules, 필요한 것만 로드) */
import debounce from 'lodash/debounce';
// 딱 debounce 함수 하나만 가져옴 (< 1KB)
최신 번들러(Webpack 5, Vite)는 import { A } from 'lib' 형태로 쓰면 안 쓰는 B, C 함수를 자동으로 제거합니다(Tree Shaking). 하지만 Lodash 같은 구형 라이브러리는 lodash-es를 쓰거나 직접 경로(cherry-pick)로 접근해야 합니다.
반드시 컴포넌트가 아니더라도, 특정 함수 실행 시점에 라이브러리를 로드할 수도 있습니다.
예를 들어, 엑셀 다운로드 기능은 사용자가 버튼을 누를 때만 xlsx 라이브러리가 필요합니다.
function ExcelButton() {
const downloadExcel = async () => {
// 버튼 클릭 시점에 라이브러리 다운로드 시작!
const XLSX = await import('xlsx');
const wb = XLSX.utils.book_new();
// ... 엑셀 저장 로직
XLSX.writeFile(wb, 'report.xlsx');
};
return <button onClick={downloadExcel}>엑셀 다운로드</button>;
}
이 기법을 활용하면 초기 로딩 속도를 극적으로(0.x초 단위로) 줄일 수 있습니다.
눈으로 보지 않으면 모릅니다. 번들 사이즈를 시각화하세요.
Next.js (@next/bundle-analyzer):
next.config.js에 플러그인을 추가하고, 빌드할 때 환경변수를 줍니다.
ANALYZE=true npm run build
실행하면 브라우저에 알록달록한 지도(Treemap)가 뜹니다.
lodash가 여러 버전으로 중복 포함되었는지 확인.여기서 moment.js나 framer-motion 같은 뚱뚱한 녀석들을 찾아내서 "더 가벼운 대안(Day.js 등)"으로 교체하는 게 최적화의 시작입니다.
번들 크기 문제가 얼마나 치명적인지 보여주는 사례가 있다. SaaS 대시보드를 런칭했을 때 첫 진입 이탈률(Bounce Rate)이 70%가 넘었고, 평균 로딩 시간이 3.5초였다고 한다. (3초 넘으면 53%가 이탈한다는 통계와 맞닿아 있다.)
원인을 분석하니, 대시보드 내부에서 쓰는 react-vis (차트 라이브러리)와 mapbox-gl (지도 라이브러리)가 로그인 페이지까지 침범해 있었습니다.
이 두 라이브러리만 합쳐도 2MB가 넘었습니다.
DashboardApp)과 랜딩 페이지(LandingPage)의 진입점(Entry Point) 자체를 나눴습니다.결과: 초기 로딩 시간 0.8초 달성. 번들 크기를 줄이면 이탈률이 크게 개선된다는 사례가 있다 — 70%에서 35%로 절반 수준까지 줄어든 경우도 있다고 한다. 교훈: 코드가 아무리 아름다워도, 로딩이 느리면 아무도 안 봅니다.
번들 사이즈를 아무리 줄여도, 메인 배너 이미지 하나가 3MB라면 말짱 도루묵입니다. 브라우저 리소스 로딩 관점에서는 JS 번들과 이미지가 대역폭을 경쟁하기 때문입니다.
loading="lazy" 속성을 줘서 나중에 받게 하세요.JS 다이어트와 이미지 다이어트는 병행해야 효과가 있습니다.
React.lazy).import * 대신 필요한 함수만 쏙쏙 뽑아 써라.bundle-analyzer로 범인을 색출해라.사용자는 당신의 코드를 다 읽고 싶어 하지 않습니다. 지금 당장 필요한 것만 보여주세요.