들어가며
프론트엔드 애플리케이션에서 번들 사이즈는 사용자 경험에 직접적인 영향을 미칩니다. 번들이 크면 초기 로딩 시간이 길어지고, 특히 모바일 환경에서는 그 차이가 더욱 두드러집니다. 이번 글에서는 모노레포 환경에서 lodash의 import 방식을 변경하여 번들 사이즈를 547KB에서 97KB로 약 82% 감소시킨 경험을 공유합니다.
문제 발견
저희 프로젝트는 여러 개의 앱을 포함하는 Vite 기반 모노레포 구조입니다. 코드 리뷰를 하던 중 Tree shaking으로 번들 사이즈를 줄일 수 있는 가능성을 발견했습니다. 이를 확인하기 위해, 먼저 번들 분석 환경을 구축했습니다.
번들 분석 도구 설치
번들 시각화를 위해 rollup-plugin-visualizer를 devDependency로 설치했습니다. Vite는 내부적으로 Rollup을 사용하므로, Rollup 플러그인을 활용하여 번들 시각화를 쉽게 추가할 수 있습니다.
yarn add -D rollup-plugin-visualizer
Vite 설정에 Visualizer 플러그인 추가
각 앱의 vite.config.ts에 ANALYZE 환경 변수가 설정된 경우에만 visualizer 플러그인이 활성화되도록 조건부로 추가했습니다.
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
react(),
...(process.env.ANALYZE
? [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
]
: []),
],
});
각 옵션의 의미는 다음과 같습니다:
| 옵션 | 설명 |
|---|---|
filename | 분석 결과를 dist/stats.html로 출력 |
open | 빌드 완료 후 브라우저에서 자동으로 열기 |
gzipSize | gzip 압축 후 사이즈 표시 |
brotliSize | brotli 압축 후 사이즈 표시 |
분석용 빌드 스크립트 추가
각 앱의 package.json에 분석용 빌드 명령어를 추가했습니다.
{
"scripts": {
"build:analyze": "ANALYZE=true yarn build"
}
}
yarn build:analyze를 실행하면 일반 프로덕션 빌드가 수행된 후, dist/stats.html이 생성되어 브라우저에서 treemap 형태의 번들 시각화 결과가 자동으로 열립니다. 이를 통해 어떤 라이브러리가 번들에서 얼마나 차지하는지 한눈에 파악할 수 있습니다.
분석 결과
이렇게 분석해 본 결과, lodash 라이브러리가 각 앱의 번들에서 상당한 비중을 차지하고 있었습니다.
원인은 간단했습니다. 코드베이스 전체에서 lodash를 아래와 같이 Named Import 방식으로 사용하고 있었던 것입니다.
import { cloneDeep } from 'lodash';
import { isEqual } from 'lodash';
이 방식은 실제로 사용하는 함수가 cloneDeep 하나뿐이더라도 lodash 전체 라이브러리(약 547KB)가 번들에 포함되는 문제를 야기합니다.
Tree Shaking이란?
Tree Shaking은 번들러(Webpack, Vite/Rollup 등)가 빌드 시 사용되지 않는 코드를 제거하는 최적화 기법입니다. 나무를 흔들면 죽은 잎사귀가 떨어지는 것처럼, 사용하지 않는 코드를 “흔들어” 떨어뜨리는 개념입니다.
그런데 lodash의 메인 패키지(lodash)는 CommonJS 모듈 형식으로 작성되어 있어, ES Modules 기반의 Tree Shaking이 제대로 동작하지 않습니다. 따라서 Named Import를 사용하더라도 번들러는 전체 라이브러리를 포함시킬 수밖에 없습니다.
해결 방법
1. Import 경로 변경
해결 방법은 놀라울 정도로 단순합니다. lodash의 개별 함수 모듈에서 직접 import하도록 경로를 변경하면 됩니다.
- import { cloneDeep } from 'lodash'
+ import cloneDeep from 'lodash/cloneDeep'
- import { isEqual } from 'lodash'
+ import isEqual from 'lodash/isEqual'
lodash/cloneDeep처럼 개별 경로로 import하면, 번들러는 해당 함수의 코드만 포함시킵니다. 전체 라이브러리를 로드할 필요가 없어지는 것이죠.
2. 적용 규모
이 변경은 결코 한두 파일의 수정이 아니었습니다. 모노레포 전체에 걸쳐 총 1,012개의 파일을 수정해야 했습니다.
| 앱/패키지 | 수정된 파일 수 |
|---|---|
| 앱 A | 394 |
| 앱 B | 300 |
| 앱 C | 182 |
| 앱 D | 114 |
| 앱 E | 9 |
| 코드 생성 도구 | 5 |
| 공유 패키지/설정 | 8 |
3. ESLint 규칙으로 재발 방지
문제를 수정하는 것만큼 중요한 것이 같은 문제가 다시 발생하지 않도록 방지하는 것입니다. ESLint의 no-restricted-imports 규칙을 공유 설정에 추가하여, lodash에서 직접 import하는 코드가 추가되면 에러가 발생하도록 했습니다.
// ESLint 공유 설정
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'lodash',
message:
"Tree Shaking을 위해 import { fn } from 'lodash' 대신 import fn from 'lodash/fn' 형식으로 사용해 주세요.",
},
],
},
],
이 규칙 덕분에 개발자가 실수로 import { cloneDeep } from 'lodash' 형태의 코드를 작성하면, 린트 단계에서 즉시 경고를 받게 됩니다.
ESLint의 import 관련 규칙을 더 자세히 알고 싶다면, eslint-plugin-import도 함께 참고해 보세요. 또한, 모노레포 환경에서의 ESLint 설정은 VSCode에서 Monorepo를 위한 ESLint 설정하기에서 다루고 있습니다.
4. 코드 생성 도구 업데이트
프로젝트에서 사용하는 코드 자동 생성 도구(scaffolding)의 템플릿도 함께 수정했습니다. 새로운 페이지나 기능을 생성할 때도 처음부터 올바른 import 방식이 적용되도록 하여, 개발 워크플로우 전체에 걸쳐 일관성을 확보했습니다.
결과
번들 사이즈 변화
lodash가 번들에서 차지하는 크기가 극적으로 감소했습니다.
547KB → 97KB (약 82% 감소)
모노레포 내 모든 앱에서 동일한 수준의 개선이 확인되었습니다.
기대 효과
- 초기 로딩 속도 향상: 약 450KB의 JavaScript 감소는 파싱 및 실행 시간 단축으로 이어집니다.
- 네트워크 비용 절감: gzip 압축 후에도 유의미한 전송량 감소가 예상됩니다.
- 모바일 사용자 경험 개선: 제한된 네트워크 환경에서 특히 효과적입니다.
교훈 및 시사점
1. 작은 변경, 큰 효과
import 경로 한 줄의 변경이 번들 사이즈를 82%나 줄일 수 있다는 점은 놀랍습니다. 프론트엔드 최적화에서 “낮게 매달린 과일(low-hanging fruit)“의 대표적인 사례라고 할 수 있습니다.
- 낮게 매달린 과일(low-hanging fruit): 적은 노력으로 큰 성과를 얻을 수 있는 작업
2. 번들 분석은 필수
문제를 인식하려면 먼저 측정해야 합니다. rollup-plugin-visualizer와 같은 도구를 활용한 정기적인 번들 분석이 최적화의 출발점입니다.
3. 규칙으로 강제하라
코드 리뷰만으로는 모든 케이스를 잡기 어렵습니다. ESLint 규칙을 통해 자동화된 검증을 추가함으로써, 팀 전체가 별도의 노력 없이도 올바른 패턴을 유지할 수 있습니다.
4. 코드 생성 도구도 함께 업데이트
프로젝트에서 scaffolding 도구를 사용한다면, 최적화된 패턴이 템플릿에도 반영되어야 합니다. 그렇지 않으면 새로운 코드가 생성될 때마다 동일한 문제가 재발합니다.
5. 대안도 고려하라
이번에는 import 경로 변경으로 해결했지만, 다른 접근 방법도 존재합니다.
- lodash-es: ES Modules 형식의 lodash로, Named Import를 사용해도 Tree Shaking이 가능합니다.
- babel-plugin-lodash / eslint-plugin-lodash: 빌드 시 자동으로 import를 변환해주는 플러그인입니다.
- 네이티브 대체:
structuredClone()(cloneDeep 대체),Object.is()(isEqual 부분 대체) 등 브라우저 내장 API를 활용하는 방법도 있습니다.
마무리
프론트엔드 성능 최적화는 거창한 아키텍처 변경이 아니라, 이번 사례처럼 세심한 코드 관리에서 시작되는 경우가 많습니다. 번들 분석을 통해 문제를 발견하고, 체계적으로 수정하며, 린트 규칙으로 재발을 방지하는 이 사이클이 건강한 프론트엔드 코드베이스를 유지하는 핵심입니다.
만약 여러분의 프로젝트에서도 lodash를 사용하고 있다면, 한번 번들 분석을 해보시길 추천합니다. 어쩌면 지금 바로 수백 KB를 줄일 수 있을지도 모릅니다.
이 외에도 프론트엔드 성능을 개선하는 다양한 방법이 있습니다. 이미지 포맷 최적화나 웹 폰트 로드 최적화, React 렌더링 성능 최적화 등도 함께 살펴보시면 좋습니다.
또 Lighthouse를 사용하면 성능 측정을 할 수 있습니다. 자세한 내용은 Lighthouse 성능 최적화 종합 가이드를 참고하세요.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.