들어가며
기능만 쌓다가 어느 순간 FCP가 너무 느려진 걸 체감하게 됐습니다.
연말에 묵힌 때를 지울 겸 메인 번들 최적화 작업을 진행했고 그 과정을 공유합니다.




1. SideEffects 필드

미사용 모듈(dead code, 죽은 나뭇잎)을 제거하는 것을 죽은 나뭇잎을 제거하는 것에 비유하여 tree shaking이라고 합니다. 트리쉐이킹을 위해선 미사용 코드를 제거했을 때 문제가 없음을 모듈 번들러에 알려야 하는데 그 필드가 바로 sideEffects 필드입니다.
이 맥락에서 말하는 대표적 side-effects로는 아래와 같은 상황들이 있습니다.
- CSS 가져오기
- 전역 객체를 수정하는 폴리필
- 글로벌 이벤트 리스너를 등록하는 라이브러리
- 프로토타입 체인을 수정하는 코드
💡 사이드 이팩트는 하나 이상의 export를 내보내는 것 이외에 import할 때 특별한 동작을 수행한느 코드를 말합니다.
예를 들어 폴리필이 있습니다. 폴리필은 전체 스코프에 영향을 미치며 일반적으로 export를 제공하지 않습니다.
하지만 정적 구문 분석에서 사용하는 부분이 없는 것처럼 보여 제거하게 되면 문제가 발생합니다.
예를 들어 순수하게 미사용 코드를 제거했을 때 아무런 문제가 없다면 아래처럼 정의하면 됩니다.
이를 통해 "이 프로젝트에서는 사이드 이팩트가 없다"라는 메세지를 번들러에게 전달합니다. (참고: mark the file as side effects-free)
{
"name": "your-project",
"sideEffects": false
}
사이드 이팩트가 존재하는 코드에 대해서는 아래처럼 선언하면 됩니다.
아래 배열에 추가된 모듈은 tree-shaking에서 제외됩니다.
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
이번 작업에서 sideEffects: false를 추가했습니다. 모든 페이지에서 사용한 모듈만 번들링되면서 용량이 줄어드는 효과를 얻을 수 있었습니다
다음은 sideEffects:true 설정시 빌드 결과입니다.


다음은 sideEffects:false 설정시 빌드 결과입니다.


번들 결과를 비교해보겠습니다.
sideEffects:true인 경우, 공용 청크의 크기(1.57mb)가 크고 페이지 번들(4.77kb)이 작습니다.
sideEffects:false인 경우에는 공용 청크의 크기(1.02mb)가 작고 페이지 번들(5kb)가 커졌습니다.
first-load-js를 비교하면 sideEffects:false를 적용했을 때 일괄적으로 작아진 결과를 볼 수 있었습니다.
이는 tree-shaking을 통해 공용 번들(first load js shared by all)에서 미사용중인 모듈이 제거 되어 용량이 줄어들고
해당 모듈을 직접 사용하는 Page 모듈에서 번들링됐음을 보여줍니다. 의미있게 봐야 하는 부분은 first-load-js입니다.
화면을 로드할 때 필요한 리소스의 총량이 줄어들어 로딩 속도가 개선됨을 수치로 확인할 수 있었습니다.
2. 필요한 파일만 import하기 (배럴 파일)
배럴 파일(Barrel File)은 여러 개의 모듈을 하나의 파일로 묶어 내보내는 방법입니다.
아래와 같은 방법으로 개별적 모듈을 동일 경로에서 가져올 수 있습니다.
// ./src/shared/lib/index.ts
export { default as sleep } from './sleep'
export { default as format } from './format'
export { default as parse } from './parse'
하지만 이런 방식의 import는 배럴 파일에 선언된 모든 모듈을 한번에 export한다는 문제가 발생합니다.
예를 들어 shared/ui 라는 배럴 파일이 존재한다고 가정해보겠습니다
// ./src/shared/ui/index.ts
export { default as Button } from './button'
export { default as Text } from './text'
export { default as Link } from './link'
// ./src/shared/index.ts
export * from './lib'
export * from './ui'
lib 폴더에 있는 format 함수만 필요한 상황에 다음과 같이 import를 하게 될 경우 ui에 정의된 모듈까지 함께 import 햐게 됩니다.
따라서 아래 예시와 같이 상세 경로를 작성해주는 것이 낫습니다.
// ❌ format 함수만 사용하는 상황이지만 ui모듈까지 import된다.
import { format } from '@shared'
// ✅ format 함수만 사용하는 상황이라면 lib까지만 import하는 것이 낫다
import { format } from '@shared/lib'
이 문제와 관련하여 next.js에서 optimizePackageImports 라는 옵션을 제공합니다.
이 옵션을 지정하면 barrel exports로 인해 하나의 컴포넌트만 import해도 전체 라이브러리가 번들에 포함되는 문제를 해결할 수 있습니다.
module.exports = {
experimental: {
optimizePackageImports: ['package-name'],
},
}
3. Dynamic import
당장 사용되지 않는 의존성을 동적 import를 통해 번들에서 의존성을 분리시키는 방법입니다.
예를 들어 상세 페이지에서 차트 UI가 특정 고객에게만 나와야 하는 상황이라면 어떨까요?
정적 분석에서는 특정 분기에서만 사용되는 모듈을 구분하지 못합니다. 이런 부분을 동적 가져오기를 적용하면 번들에서 의존성이 분리되면서 초기 로드 속도를 앞당길 수 있습니다.
next.js의 app.js 번들에서 불필요한 grid, chart, xlsx 관련 의존성이 포함되어 번들 크기가 2mb가 되었습니다.
grid와 chart같은 경우 라이센스 초기화 로직을 동적 로딩을 통해 메인 번들에서 분리시켰습니다.
xlsx-color같은 경우는 app.js에서 import되고 있었으나 사용되는 로직이 없었기에 제거했습니다.
이를 통해 2mb에서 774.69kb로 60%가량 메인 번들 사이즈를 줄일 수 있었습니다.


4. 중요하지 않은 CSS(non critical css) 지연
css파일은 렌더링을 차단하는 리소스입니다. 따라서 첫 로딩에서 불필요한 큰 스타일 시트가 포함됐는지 확인하고 당장 필요없는 스타일 시트의 로드를 지연시켜야합니다.
초기 로딩에서 필요한가에 따라 critical css, non critical css로 분류합니다.
제가 관리하는 프로젝트에서는 mapbox라는 서드파티 라이브러리를 사용하고 있었는데요. 이 라이브러리 스타일로직이 app.js에서 초기화되고 있었습니다.
// _app.tsx
// ❌ 특정 화면에서만 사용되는 스타일 코드가 app.tsx에 포함되어 공용 번들에 포함된다.
import 'mapbox-gl/dist/mapbox-gl.css';
실제 작업전 빌드를 확인해보면 공용 css의 크기가 119kb인것을 알 수있습니다.
이 스타일 코드를 지도 컴포넌트 레벨로 이동했고 공용 css가 16.5kb로 줄어든 것을 확인할 수 있었습니다.


마무리
아직 추가적으로 작업할 부분이 남아있습니다.
- 분리된 모듈에 대한 프리로딩으로 페이지 전환 시간을 단축시키는 방법
- 무거운 의존성 경량화(momentjs->dayjs)
- 의존성 정리를 통한 중복 의존성 제거
위 작업들이 남은 작업입니다. 현재 프로젝트를 기준으로 생각해보면 레거시 로직에 물려있는 의존성을 제거하는게 가장 드라마틱할 것 같습니다. (레거시 디자인 시스템, 레거시 지도 라이브러리(google-map), ant-design 등등... )
참고
https://web.dev/articles/optimize-lcp?hl=ko
최대 콘텐츠 렌더링 시간 최적화 | Articles | web.dev
LCP 분석 및 개선이 필요한 주요 영역을 파악하는 방법에 관한 단계별 안내
web.dev
https://web.dev/articles/defer-non-critical-css?hl=ko
중요하지 않은 CSS 연기 | Articles | web.dev
주요 렌더링 경로를 최적화하고 콘텐츠가 포함된 첫 페인트 (FCP) 개선을 위해 중요하지 않은 CSS를 연기하는 방법을 알아보세요.
web.dev
your-nextjs-bundle-will-thank-you
Your Next.js bundle will thank you
If you are having problems with an extremely huge bundle size for your Next.js application, this article could be a lifesaver for you.
askides.com
Tree Shaking | 웹팩
웹팩은 모듈 번들러입니다. 주요 목적은 브라우저에서 사용할 수 있도록 JavaScript 파일을 번들로 묶는 것이지만, 리소스나 애셋을 변환하고 번들링 또는 패키징할 수도 있습니다.
webpack.kr
'react' 카테고리의 다른 글
| [설계] 모듈 응집도와 단방향 의존성을 통한 유지보수 비용 줄이기 (0) | 2025.12.18 |
|---|---|
| [설계] UI로직에서 비즈니스 로직 분리하기 (0) | 2025.12.17 |
| 2025년에 돌아보는 react-query (0) | 2025.12.10 |
| FSD Architecture TypeScript 예시 (0) | 2025.08.29 |
| [설계] 객체지향의 유연한 구조 설계, TypeScript와 React로 이해하기 (0) | 2025.08.24 |