코드 분할과 트리 셰이킹: 언제 나누고 무엇을 뺄까
코드 분할은 코드를 받는 시점을 나누고 트리 셰이킹은 필요 없는 코드를 출력에서 제외한다는 차이를 이해하고, React와 React Native에서 실제 산출물을 판단한다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
이 글은 다음 네 문장을 여러 번 떠올릴 수 있으면 충분하다.
- 코드 분할은 남길 코드를 언제 받을지 나누는 일이다.
- 트리 셰이킹은 없어도 되는 코드를 출력에서 빼는 일이다.
import()는 비동기 불러오기 문법이지, 모든 환경의 별도 파일 보장이 아니다.- 결과는 개발 서버가 아니라 운영 빌드 결과와 실제 사용자 경로에서 확인한다.
두 최적화는 서로 다른 질문에 답한다
코드 분할: 남길 코드 ── 로딩 경계로 나누기 ──> 처음 받을 조각 + 나중에 받을 조각
트리 셰이킹: 후보 코드 ── 필요성과 부수 효과 판단 ──> 남길 코드 - 안전하게 뺄 코드둘은 함께 적용될 수 있다. 나중에 받을 청크 안에서도 미사용 내보내기가 빠질 수 있다. 반대로 트리 셰이킹이 잘되어도 출력이 반드시 여러 파일로 나뉘는 것은 아니다.
코드 분할은 언제 받을지를 나눈다
정적 import로 연결된 코드는 보통 시작 파일과 함께 필요한 관계로 분석된다.
import { formatPrice } from "./price.js"
showPrice(formatPrice(42))반면 보고서 화면이 사용자가 버튼을 누른 뒤에만 필요하다고 하자.
button.addEventListener("click", async () => {
const { renderReport } = await import("./report.js")
renderReport()
})빌드 도구가 이 동적 경계를 코드 분할에 사용하면 처음 필요한 코드와 보고서 코드를
서로 다른 청크에 둘 수 있다. Webpack 공식 문서는 여러 시작점, 공통 코드 분리와
동적 import()를 대표적인 코드 분할 방식으로 설명한다.
그러나 “파일 하나를 늦췄으니 무조건 빨라졌다”는 결론은 아직 이르다.
공유 의존성도 고려해야 한다. 두 늦은 화면이 같은 라이브러리를 쓴다면 도구는 그 코드를 공통 청크로 만들거나 각 청크에 다르게 배치할 수 있다. 정확한 파일 개수와 이름은 설정과 도구 버전에 따라 달라질 수 있으므로 소스 코드만 보고 단정하지 않는다.
동적 import는 청크 보장이 아니다
따라서 다음 두 문장을 구분한다.
- 언어의 약속:
import()결과를 비동기로 기다려 내보낸 값의 집합을 얻는다. - 도구의 약속: 그 요청을 별도 청크로 만들지, 합칠지, 어떻게 가져올지 정한다.
이 구분을 놓치면 Web에서 본 예제를 React Native에 그대로 적용하거나, 개발 서버의 요청 모양만 보고 릴리스 앱도 같은 파일 구조라고 오해하기 쉽다.
트리 셰이킹은 필요성을 증명한다
트리 셰이킹은 “실행하지 않을 것 같은 코드”를 추측해 전부 지우는 과정이 아니다. 정적 파일 관계와 실제 사용을 따라가며 코드가 없어도 관찰 가능한 동작이 바뀌지 않는지 보수적으로 판단한다.
// price.js
export const unusedLabel = "사용하지 않는 문구"
export const formatPrice = (value) => `${value}원`
// main.js
import { formatPrice } from "./price.js"
console.log(formatPrice(42))운영 빌드가 formatPrice의 사용을 확인하고 unusedLabel의 초기화에 부수 효과가 없다고
판단하면 unusedLabel을 출력에서 제외할 수 있다. ECMAScript Module(ESM)의 정적인
import와 export는 어떤 이름이 연결되고 쓰이는지 빌드 전에 분석할 단서를 제공한다.
트리 셰이킹과 축소(minification, 공백을 없애고 이름을 짧게 만드는 처리)도 다르다. 축소는 남은 코드의 표현을 작게 만들고, 트리 셰이킹은 코드가 남을 필요가 있는지 판단한다. 운영 빌드에서 두 작업이 함께 실행되어도 같은 최적화는 아니다.
부수 효과가 있으면 사용되지 않아도 남아야 한다
다음 파일은 내보내는 값이 없어도 불러오는 행위 자체가 필요하다.
// setup.js
globalThis.appReady = true
// main.js
import "./setup.js"setup.js의 최상위 코드는 파일을 불러올 때 실행되면서 전역 상태를 바꾼다. 스타일 파일
import, 폴리필 설치, 전역 이벤트 등록도 같은 범주에 들어갈 수 있다.
여기서 말하는 부수 효과는 모듈을 불러와 최상위 코드를 실행했을 때 생기는 관찰 가능한
변화다. React의 Effect나 useEffect라는 기능 자체를 가리키는 말은 아니다. React
Effect 안의 코드도 외부 상태를 바꾸면 일반적인 의미의 부수 효과를 만들 수 있지만,
이 글은 빌드 도구가 모듈 코드를 제거해도 되는지를 판단하는 범위에 집중한다.
그러므로 출력 크기를 줄이기 위해 무조건 sideEffects: false를 추가하지 않는다.
- 파일을 불러올 때 즉시 실행되는 코드와 스타일 같은 자산 import를 찾는다.
- 부수 효과가 있는 파일은 명시적으로 보존한다.
- 최소 소비 애플리케이션을 운영 설정으로 빌드한다.
- 산출물뿐 아니라 필요한 스타일과 초기화 동작도 실행해서 확인한다.
운영 빌드 결과로 확인한다
이 글의 실행 예제는 Vite 7.3.6 운영 빌드 결과를 재현하도록 고정한 최소 예제다. 다음 입력을 제공한다.
main.js ── 정적 import ──> price.js
│
├── 실행 목적 import ──> setup.js
│
└── 동적 import ──────> report.jsprice.js에는 사용하는 내보내기와 사용하지 않는 내보내기가 함께 있다.setup.js는 전역 값을 바꾸는 부수 효과가 있다.report.js는 사용자 행동 뒤 동적으로 불러온다.
이 검증은 “Vite가 언제나 파일 두 개를 만든다”는 보장이 아니다. 예제 입력, Vite 버전과 운영 설정에서 세 가지 판단을 관찰한 증거다. Vite 공식 지원 표에서 7.3은 중요 수정과 보안 패치를 받는 범위다. 파일 이름이 바뀌어도 반드시 유지되어야 할 결과인 다음 불변 조건을 검사한다.
동적 대상은 진입 청크에 없음
동적 대상은 별도 동적 청크에 있음
미사용 순수 내보내기는 전체 출력에 없음
필요한 부수 효과는 진입 청크에 있음React에서는 화면 경계와 로딩 경계를 맞춘다
React의 lazy는 컴포넌트 코드 로드 함수를 첫 렌더 시도까지 늦춘다.
import { lazy, Suspense } from "react"
const Report = lazy(() => import("./Report.js"))
export function ReportArea() {
return (
<Suspense fallback={<p>보고서를 불러오는 중입니다.</p>}>
<Report />
</Suspense>
)
}이 예제의 Report.js는 컴포넌트를 **기본 내보내기(default export)**로 제공해야 한다.
lazy가 받은 Promise는
성공할 때 .default에 유효한 React 컴포넌트가 있는 객체를 제공해야 하기 때문이다.
불러오기가 실패해 Promise가 거부되면 오류는 가장 가까운 Error Boundary(렌더링 오류를
받아 복구 UI를 보여 주는 React 경계)로 전달된다. Suspense의 fallback은 기다리는
동안의 UI이지, 실패한 import를 처리하는 오류 경계가 아니다.
좋은 분할 경계는 단순히 큰 컴포넌트가 아니라 사용자가 늦게 진입하며 기다림을 이해할 수 있는 화면 경계다. 너무 작은 컴포넌트마다 나누어 별도 지연 청크가 생기면 요청과 대기 상태가 늘 수 있고, 이미 보던 UI가 불필요하게 fallback으로 바뀔 수 있다. 라우트나 무거운 도구처럼 독립적으로 늦게 쓰는 기능에서 시작하고 실제 전송과 상호작용 대기를 측정한다.
React Native에서는 개발과 릴리스를 구분한다
같은 import() 문법을 써도 Metro의 현재 계약은 Web 빌드 도구와 다르다.
따라서 React Native에서 lazy(() => import(...))가 보인다는 이유만으로 다음을
단정하지 않는다.
- 앱 설치 파일의 JavaScript가 화면별 파일로 작아졌다.
- 사용자가 해당 화면을 열 때 서버에서 코드를 처음 받는다.
- Web과 같은 청크 캐시·오류 복구 전략을 쓸 수 있다.
React의 렌더링 대기 경계, Metro 개발 서버의 lazy loading, 실제 릴리스 출력 형식은 서로 다른 층이다. 릴리스 크기나 시작 성능이 목표라면 실제 Android·iOS 릴리스 산출물과 기기 시작 구간을 측정해야 한다.
파일 수가 아니라 사용자 경로를 측정한다
| 관찰한 문제 | 먼저 확인할 것 | 판단을 바꿀 신호 |
|---|---|---|
| 첫 화면 JavaScript가 큼 | 운영 시작 청크의 코드 구성과 실제 사용 | 늦게 쓰는 큰 기능이 시작 청크에 남아 있음 |
| 기능을 처음 열 때 느림 | 동적 청크 크기, 요청 시작과 완료, 코드 실행 시간 | 초기 비용 감소보다 상호작용 대기가 더 큼 |
| 운영에서 스타일·초기화가 사라짐 | sideEffects 선언과 실행 목적 import | 필요한 스타일·폴리필·등록 코드가 출력에 없음 |
| 미사용 코드가 계속 남음 | ESM 보존 여부, 실제 참조, 분석을 막는 부수 효과 | 사용하지 않는 표식이 운영 산출물에 남아 있음 |
| React Native 개발만 빨라짐 | Metro 릴리스 출력과 실제 기기 시작 구간 | 개발 lazy loading과 릴리스 결과가 다름 |
검증 순서는 간단히 유지한다.
- 목표를 “파일 수”가 아니라 “어느 사용자 경로의 어떤 비용”으로 쓴다.
- 운영 설정으로 빌드하고 진입·동적 청크와 미사용 코드 표식을 확인한다.
- 초기 화면과 늦은 기능을 실제 네트워크 조건에서 각각 실행한다.
- 로딩 실패와 필요한 부수 효과까지 회귀 검증한다.
이 글의 경계
이번 글은 앞선 글에서 만든 파일 관계의 코드를 언제 받을지 나누는 판단과 출력에서 빼도 되는지 판단하는 과정을 구분했다. 다음 주제는 분리한다.
- 변환된 청크의 위치를 원본 줄과 열로 되돌리는 방법: 다음 글인 소스 맵
- Webpack·Vite·Metro의 전체 파이프라인과 설정 비교: 6편의 주제
- 같은 입력으로 같은 산출물을 다시 만드는 방법: 재현 가능한 빌드
- 배포 뒤 오래 열린 클라이언트가 옛 청크를 요청할 때의 운영 전략: 배포와 롤백
마지막으로 두 문장만 다시 기억한다. 코드 분할은 언제 받을지를 나눈다. 트리 셰이킹은 없어도 되는 코드를 뺀다.
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01코드 분할과 트리 셰이킹은 무엇이 다른가?
정답
코드 분할은 남길 코드를 언제 받을 출력에 둘지 나누고, 트리 셰이킹은 없어도 동작이 달라지지 않는 코드를 출력에서 제외한다.
관련 설명 다시 읽기왜 그런가
하나는 로딩 시점과 청크 경계를 정하고 다른 하나는 코드의 필요성과 부수 효과를 판단한다. 둘은 같은 운영 빌드에서 함께 일어날 수 있다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02import()를 쓰면 모든 환경에서 반드시 별도 파일이 생기는가?
정답
아니다. import()는 모듈을 비동기로 불러오는 표현이며 실제 청크 생성과 전달 방식은 빌드 도구, 프레임워크, 실행환경과 설정이 정한다.
관련 설명 다시 읽기왜 그런가
Webpack은 이를 분할 경계로 쓸 수 있지만 현재 Metro의 React Native 자동 분할은 개발 빌드에만 적용되고 릴리스 빌드에는 영향을 주지 않는다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03Webpack용 package.json에 sideEffects false를 쓰면 언제나 안전하게 더 작은 출력을 얻는가?
정답
아니다. Webpack에서 모든 해당 파일에 제거하면 안 되는 최상위 코드 실행이 없다는 사실을 작성자가 보증할 때만 안전하다.
관련 설명 다시 읽기왜 그런가
스타일 적용, 폴리필, 전역 등록 같은 부수 효과를 빠뜨리면 운영 최적화에서 필요한 동작이 사라질 수 있다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
04React.lazy와 import()를 React Native에 쓰면 릴리스 앱도 화면별 파일을 네트워크로 받는가?
정답
현재 Metro 공식 동작만으로는 아니다. React의 렌더링 대기 경계와 Metro 릴리스 산출물의 분할 여부를 따로 확인해야 한다.
관련 설명 다시 읽기왜 그런가
React.lazy는 컴포넌트 코드 로드 시점을 늦추지만 실제 파일 생성과 전송은 플랫폼 도구의 책임이다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- ECMAScript 2026 — Import CallsEcma International · 표준 명세 · 확인 2026-08-04
- Code Splittingwebpack · 공식 문서 · 확인 2026-08-04
- Tree Shakingwebpack · 공식 문서 · 확인 2026-08-04
- Configuration Options — treeshakeRollup · 공식 문서 · 확인 2026-08-04
- Configuration Options — treeshake.moduleSideEffectsRollup · 공식 문서 · 확인 2026-08-04
- Building for ProductionVite · 공식 문서 · 확인 2026-08-04
- Releases — Supported VersionsVite · 공식 문서 · 확인 2026-08-04
- lazyReact · 공식 문서 · 확인 2026-08-04
- SuspenseReact · 공식 문서 · 확인 2026-08-04
- Module API — import()Metro · 공식 문서 · 확인 2026-08-04