-
[Study-Mate] 성능 최적화 3. 번들링 / 코드 스플리팅NextJS 2026. 4. 12. 18:33
1. 번들링과 스플리팅
1-1. 번들링이란
1-2. 스플리팅이란
2. 번들링과 스플리팅의 방법
2-1. 번들링의 방법
2-2. 스플리팅의 방법
3. 번들 분석 도구
4. 실제 적용
4-1) 분석4-2) 과정
4-3) 결과
1. 번들링과 스플리팅

1-1) 번들링이란?
각 페이지마다 나눠져 있는 코드를 하나의 파일로 합치는 것이다.
만약 개발자의 환경처럼 파일이 전부 나눠져 있는 상태로 서비스를 운영한다면
페이지 요청마다 여러 파일의 요청이 필요하게 되고 여러번의 HTTP 요청이 발생할 것이다. 하지만 번들링으로 파일을 하나로 합친다면 하나의 파일만으로 요청을 처리할 수 있어서 속도와 효율성을 올릴 수 있다. 개발자는 평소처럼 컴포넌트를 나눠서 작업하고, 빌드 시 번들러(Webpack, Turbopack)가 자동으로 합쳐준다1-2) 스플리팅이란?
코드 스플리팅이란 코드를 분할 하는 작업이다. 모든 페이지를 하나의 파일로 합친다면 전송은 한번으로 처리할 수 있으나 그 파일의 크기가 너무 커지게 되고 필요 없는 코드까지 모두 보내게 되어 속도와 효율성이 떨어지게 된다. 이때 코드 스플리팅으로 번들링된 하나의 파일을 각 요청에 필요한 번들만 쓸 수 있게 분할하여 저장해 두는 것이다. 이렇게 하면 네트워크의 요청마다 필요한 번들만 보내어 속도와 효율성을 올릴 수 있다.
NextJS는 App Router 기준으로 각 라우트 경로(/, /posts, /posts/[id] 등)에 해당하는 page.tsx마다 별도 번들로 자동 분리해준다.(Route-based splitting)2. 번들링과 스플리팅의 방법
2-1) 번들링의 방법
여기서 이미 개발자의 환경에서는 파일이 나눠져 있는데 왜 굳이 하나로 합쳤다가 다시 스플리팅 하는 것인지에 대해 의문을 가질 수 있다 번들링은 단순히 코드를 합치는 것 뿐만이 아니라 아래의 작업들도 함께 한다.
더보기- 의존성 그래프 생성 (Dependency Graph)
번들러가 가장 먼저 하는 작업이다. 엔트리 파일(예: app/page.tsx)에서 시작해서 그 파일이 import하는 파일을 따라가며 프로젝트 전체의 모듈 관계를 분석한다.// page.tsx import Header from './Header' // Header.tsx 추가 import Button from './Button' // Button.tsx 추가 // Header.tsx import { useAuth } from './hooks' // hooks.ts 추가더보기이렇게 모든 파일의 관계가 파악되면 이후 Tree shaking, 파일 합치기 등의 작업을 수행할 수 있게 된다.
- 언어 변환 (Transpilation)
브라우저는 JSX, TypeScript, SCSS를 직접 이해하지 못한다. 따라서 번들러는 각 파일 형식에 맞는 도구를 사용하여 브라우저가 이해할 수 있는 형태로 변환한다.
// 변환 전 (TSX) const Button = ({ text }: { text: string }) => { return <button>{text}</button> } // 변환 후 (JS) const Button = ({ text }) => { return React.createElement('button', null, text) }- 파일 합치기 + Scope hoisting (Module Concatenation)
의존성 그래프와 언어 변환이 완료된 후, 번들러는 분석된 모든 모듈을 하나의 파일로 합친다. 이때 Scope hoisting도 함께 수행되는데, 기존에는 모듈마다 별도의 함수 스코프로 감싸져 있던 것을 하나의 스코프로 통합하여 런타임 성능을 향상시킨다.
// 합치기 전 (모듈마다 별도 스코프) (function(module) { var x = 1 })(module) (function(module) { var y = 2 })(module) // Scope hoisting 후 (하나의 스코프로 통합) var x = 1 var y = 2함수 생성 비용이 줄어들고 JS 엔진이 코드를 더 효율적으로 최적화할 수 있게 된다.
- Tree shaking : 필요없는 코드 제거
ES Module의 정적 분석을 이용하여 import했지만 실제로 사용하지 않는 코드를 번들에서 제거한다. 프로덕션 번들 용량을 줄여 로딩 속도를 개선한다.
// utils.ts export function a() { ... } export function b() { ... } export function c() { ... } // page.tsx import { a } from './utils' a() // b, c는 사용하지 않음 → 번들에서 제거- CSS 최적화 (PurgeCSS) : 사용하지 않는 CSS 제거
실제로 사용한 CSS 클래스만 번들에 포함시키고 사용하지 않는 CSS를 제거한다. Tailwind CSS는 빌드 시 PurgeCSS가 자동으로 적용된다.
/* 변환 전: 모든 클래스 포함 (수천 개) */ .flex { display: flex; } .grid { display: grid; } .hidden { display: none; } /* ... 수천 개의 클래스 */ /* 변환 후: 실제 사용한 클래스만 포함 */ .flex { display: flex; } .text-sm { font-size: 0.875rem; } .rounded-lg { border-radius: 0.5rem; }- Minification (Minify) : 공백, 줄바꿈, 함수명 등 축약
코드 압축 작업으로 공백, 줄바꿈, 주석을 제거하고 변수명과 함수명을 축약하여 파일 크기를 줄인다. 모든 최적화 작업이 끝난 후 마지막에 수행된다.
// 압축 전 function sayHello(name) { // 인사를 반환하는 함수 return "Hello " + name; } // 압축 후 function a(b){return"Hello "+b}- 캐싱 (Cache Busting) : 결과물 파일명에 해시값 추가
번들링의 최종 결과물에 파일명에 해시값을 추가한다. 파일 내용이 변경되면 해시값이 바뀌어 브라우저가 새로운 파일을 받아가고, 내용이 동일하면 기존 캐시를 재사용한다.
// 코드 변경 전 bundle.a1b2c3.js → 브라우저 캐시 재사용 (요청 없음) // 코드 변경 후 bundle.d4e5f6.js → 해시값이 바뀌어 브라우저가 새로 요청자주 변경되는 앱 코드와 잘 변경되지 않는 외부 라이브러리(react, lodash 등)를 별도 번들로 분리하면, 라이브러리 번들은 캐시를 오래 유지할 수 있어 불필요한 네트워크 요청을 줄일 수 있다.
이런 최적화 작업들을 거친 결과물은 개발자가 나눠놓은 원본 파일과 다르다.
최적화된 코드를 기준으로 스플리팅해야 실제 브라우저에 전달되는 번들이 최적화된 상태로 나뉘기 때문에 번들링 후 스플리팅하는 순서로 진행되는 것이다.
2-2) 코드 스플리팅의 방법
위의 번들링 이후 코드 스플리팅을 진행한다.
번들의 크기와 공유 빈도 등에 따라 번들러마다 각기 다른 방식과 기준으로 자동으로 코드를 스플리팅을 해준다.
참고로 NextJS는 Webpack을 기본으로 사용하여 코드 스플리팅을 하며 App Route 의 경우, 사용빈도와 크기에 따라 분할해주는 Webpack의 자동 스플리팅에 추가로 Route 경로에 따라 자동으로 분할(Route-based-spliging)해준다.

NextJS 번들링 / 코드 스플리팅 - Route-based splitting
라우트(경로) 단위로 번들을 분리하는 방법이다. Next.js App Router 기준으로 각 라우트 경로에 해당하는 page.tsx마다 별도 번들로 자동 분리된다. 홈 페이지 접속 시 홈 번들만, posts 페이지 접속 시 posts 번들만 전달되어 불필요한 코드를 로드하지 않는다.
app/ ├── page.tsx → home.js 번들 ├── posts/ │ └── page.tsx → posts.js 번들 └── posts/[id]/ └── page.tsx → posts-detail.js 번들- Vendor splitting
react, lodash 같은 외부 라이브러리를 앱 코드와 별도 번들로 분리하는 방법이다. 외부 라이브러리는 자주 변경되지 않기 때문에 분리해두면 캐시를 오래 유지할 수 있다. 앱 코드가 변경되어도 라이브러리 번들은 캐시를 그대로 재사용하여 불필요한 네트워크 요청을 줄인다.
framework.a1b2c3.js → react, react-dom (잘 안 바뀜 → 캐시 오래 유지) app.d4e5f6.js → 앱 코드 (자주 바뀜 → 캐시 자주 갱신)NextJS에서 기본적으로 해주지만 특정 라이브러리를 별도 청크로 강제 분리하고 싶을 때 추가 설정할 수 있다.
// next.config.js module.exports = { webpack: (config) => { config.optimization.splitChunks.cacheGroups = { tiptap: { test: /[\\/]node_modules[\\/](@tiptap)[\\/]/, name: 'tiptap', chunks: 'all', } } return config } }- Shared chunk splitting
여러 페이지에서 공통으로 사용하는 컴포넌트나 모듈을 별도 청크로 분리하는 방법이다. 예를 들어 Header, Button 같은 공통 컴포넌트가 여러 페이지에서 사용되면 자동으로 commons 청크로 묶어 중복 로드를 방지한다.
commons.js → Header, Button 등 공통 컴포넌트 home.js → 홈 페이지 전용 코드 posts.js → posts 페이지 전용 코드NextJS에서는 기본적으로 모듈이 2개 이상 페이지에서 사용될 때 분리되는데, 이 기준을 조정할 수 있다.
// next.config.js module.exports = { webpack: (config) => { config.optimization.splitChunks.minChunks = 3 // 3개 이상 페이지에서 사용될 때 분리 return config } }- dynamic import
JavaScript 표준 문법으로 컴포넌트가 실제로 렌더링되는 시점에 해당 번들을 요청하는 방법이다. 일반 import는 페이지 로드 시 무조건 함께 로드되지만, dynamic import는 필요한 시점에만 로드하여 초기 번들 크기를 줄일 수 있다. Next.js에서는 dynamic() 함수를 제공한다.
// 일반 import: 페이지 로드 시 무조건 함께 로드 import Carousel from './Carousel' // dynamic import: 컴포넌트 렌더링 시점에 로드 const Carousel = dynamic(() => import('./Carousel'))번들 분석 결과 공통 번들에 포함된 무거운 컴포넌트를 발견했을 때 적용하면 효과적이다.
- Lazy loading
이미지나 컴포넌트를 뷰포트에 진입하는 시점에 로드하는 방법이다. 초기 로딩 시 화면에 보이지 않는 리소스를 불필요하게 로드하지 않아 초기 로딩 속도를 개선한다. HTML 표준 기능으로 Next.js에 국한되지 않으며, Next.js의 <Image> 컴포넌트는 기본적으로 Lazy loading이 적용되어 있다.
// HTML 표준 <img src="example.jpg" loading="lazy" /> // Next.js Image (기본적으로 lazy loading 적용) <Image src="/example.jpg" alt="example" width={500} height={300} /> // 초기 화면에 노출되는 이미지는 priority로 즉시 로드 <Image src="/hero.jpg" alt="hero" priority />위 사진처럼 NextJS를 사용할 시
Route-based splitting, Vendor splitting, Shared chunk splitting를 어느정돈 자동으로 해주기 때문에 일반적으로 개발자가 코드 스플리팅에 관여하는 방법은 dynamic import와 Lazy loading이다.
3. 번들 분석 도구(@next/bundle-analyzer)
NextJS는 번들과 스플리팅을 분석할 수 있는 도구를 제공해준다.
3-1) 설치
@next/bundle-analyzer3-2) next.config.ts 설정
기존 코드 내용에 추가로 아래 내용을 추가하였다.
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) module.exports = withBundleAnalyzer(nextConfig)3-3) 실행 방법
터미널에 아래와 같이 입력하면 된다.
$env:ANALYZE="true"; npm run build하지만 너무 불편했기 때문에
package.json 파일에
"scripts": { ... "analyze": "cross-env ANALYZE=true next build --webpack" }를 추가하고
npm install cross-env를 설치하여
npm run analyze로 시작할 수 있게 하였다.
3-4) 결과

성공적으로 실행하고 나면 위와 같은 화면이 나오게 된다.
4. 분석과 적용
4-1) 분석
위에 설명을 했듯 NextJS에서 일반적으로 코드 스플리팅을 하는 방법은 Dynamic import와 Lazy loading이다.
그 중 Dynamic import를 해보려고 했지만 워낙 작은 프로젝트다보니 마땅히 없던 와중에 embla-carousel-react 라이브러리를 발견했다.
경로에 ./node_modules/embla... 이라고 적힌걸 보니 공통 청크로 스플리팅 된 것 같았고 실제로는 게시글 상세 페이지에서만 쓰기 때문에 개별 청크로 분리하기로 했다.
더보기**embla-carousel-react가 공통 청크로 스플리팅 된 이유**
이유가 궁금하여 찾아보았다.
// Webpack 기본 설정 (Next.js 내부) cacheGroups: { defaultVendors: { test: /[\\/]node_modules[\\/]/, // ← node_modules면 무조건 분리 priority: -10, }, default: { minChunks: 2, // 2개 이상 페이지에서 사용되면 분리 priority: -20, }, }더보기embla-carousel-react가 node_modules에 있는 외부 라이브러리니까 Vendor splitting 기준에 걸려서 공통 청크로 묶인 것으로 파악 되었다.
4-2) 해결

Shadcn에서 embla-carousel을 쓰고 있었기 때문에 해당 컴포넌트를 쓰는 곳으로 찾아가
carousel 컴포넌트에 danamic import를 설정하였다.
4-3) 결과

아주 미미한 차이이기 때문에 성능상에 큰 차이는 없었지만 기존의 공통 청크외에 ./components/ui/carousel.tsx로 경로가 적힌 청크가 추가로 분할 됐음을 확인했다.
'NextJS' 카테고리의 다른 글
[Study-Mate] 성능최적화 - 번들 최적화 (0) 2026.04.30 [Study-Mate] GitHub Actions 배포 자동화(with. docker) (0) 2026.04.06 [Study-Mate] HTTPS로 전환하기 (0) 2026.04.05 [Study-Mate] 성능 최적화 2. 이미지 최적화 (0) 2026.03.29 Zustand 도입 이유 (0) 2026.03.28