Code Splitting 이란
코드 스플리팅(Code Splitting)은 애플리케이션의 코드를 필요한 시점에 로드할 수 있도록 여러 개의 작은 파일로 분리하는 기술이다.
애플리케이션이 커질수록 CSS와 JavaScript 번들 크기가 커지는데, 이를 그대로 로드하면 초기 로딩 속도가 느려질 수 있다. 코드 스플리팅을 활용하면 초기 화면에 필요한 코드만 로드하고, 나머지 코드는 필요한 시점에 Lazy Loading 방식으로 불러올 수 있어 성능을 개선할 수 있다.
주로 Webpack과 같은 모듈 번들러를 사용해 구현하며, 이를 통해 애플리케이션의 초기 로딩 시간을 줄이고 사용자 경험을 향상시킬 수 있다.
현재 프로젝트에서, Code Splitting을 하는 이유?
리액트 어플리케이션의 경우 빌드를 통해서 파일을 배포하게 되고, 이 과정에서, 파일 크기를 최소화하는 것이 바람직하다.
파일 크기가 너무 클 경우 리액트 애플리케이션의 초기 로딩 속도가 상당히 느려지므로, 이는 결과적으로 User Experience에 영향을 미치게 된다.
pnpm run buildtsx
이렇게 빌드를 하게 되면 위의 이미지처럼 기존에 하나의 자바스크립트 파일에 내가 만든 모든 리액트 파일이 담긴 것을 알 수 있다.
사실 지금 실제 서비스로 운영하고 있는 서비스의 구조는 아래와 같다.
📦 Crabit
┣ 📂 Crabit (not-login)
┃ ┣ 📂 / (홈페이지)
┃ ┣ 📂 /crabit-challenge (크래빗 챌린지)
┃ ┣ 📂 /crabit-scholarship-card (크래빗 장학카드)
┃ ┣ 📂 /crabit-pricing (요금제)
┃ ┣ 📂 /signup (회원가입)
┃ ┣ 📂 /password-find (비밀번호 변경)
┣ 📂 Crabit(login)
┃ ┣ 📂 마이페이지 관련
┃ ┣ 📂 대시보드 관련
┃ ┣ 📂 챌린지 관련
tsx현재 제가 만든 서비스는 약 40개의 페이지로 구성되어 있지만, 로그인하지 않은 사용자가 실제로 접근할 수 있는 페이지는 6개에 불과하다.
모든 사용자가 로그인 후 서비스를 이용하는 것이 바람이지만, 현실적으로는 서비스를 사용하지 않고 랜딩 페이지만 확인하는 사용자도 상당히 많다. 이런 상황에서 로그인하지 않은 사용자 조차 불필요하게 로그인이 필요한 나머지 30개 이상의 페이지에 대한 JavaScript 번들을 다운로드를 하게 된다면, 불필요하게 서버의 자원을 낭비하여 통신이 느린 환경에서 더더욱 초기 접속 속도가 느려지는 불편함을 겪게 된다.
React 애플리케이션은 규모가 커질수록 사용하는 라이브러리와 유틸리티의 크기가 함께 증가하며, 이는 번들 크기에 직접적인 영향을 미친다. 결과적으로 사용자가 웹 페이지를 처음 방문할 때 로드해야 할 파일의 크기가 커지고, 특히 인터넷 속도가 느린 환경에서는 초기 접속 속도가 매우 느려져 사용자 경험에 부정적인 영향을 미친다.
이를 해결하기 위해 코드 스플리팅과 같은 기법을 통해 초기 로드에 필요한 코드만 불러오고, 나머지는 필요한 시점에 로드하는 방식이 필요하다.
해결 방법
1. Code Splitting과 Lazy Loading
Code Splitting과 Lazy Loading을 사용하여 초기 로딩 속도를 개선하고 불필요한 번들 로드를 줄이는 방식을 채택한다.
Lazy Loading
React.lazy를 사용하면 컴포넌트를 처음 렌더링하는 시점까지 해당 컴포넌트의 코드를 로드하지 않는다. 이는 컴포넌트를 필요한 시점에 동적으로 로딩하여 애플리케이션의 성능을 향상시킨다.
장점
- 번들을 여러 개의
chunk로 분할하여 초기 로딩 시 필요한 코드만 로드 - 데이터 사용량 절감 및 빠른 로딩으로 더 나은 사용자 경험 제공
- 중요도가 떨어지거나 당장 화면에 보이지 않는 요소들의 번들 로딩을 우선적으로 시행하지 않으면서 로딩 퍼포먼스를 최적화할 수 있음
예제
import React, { Suspense } from 'react';
// React.lazy를 통해, 컴포넌트를, Lazy Loading을 한다.
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
// 로딩을 하는 동안, 보여줄 fallback UI를 보여줄 수 있다.
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}tsxCode Splitting
Code Splitting은 클라이언트에 불필요한 코드가 전달되지 않도록 하고, 적절한 시점에 필요한 코드만 동적으로 로드할 수 있도록 분리하는 기법이다.
장점
- 큰 번들 파일을 작은 chunk로 분할
- 초기 로딩 속도 개선 및 상호작용 시간 단축
- 사용자가 빠르게 콘텐츠를 보고 상호작용할 수 있어 사용자 경험 향상
2. Suspense
Suspense는 동적으로 로드되는 컴포넌트의 로딩 상태를 처리하기 위한 컴포넌트다. 로딩 중 화면에 표시할 UI를 fallback으로 정의하여 사용자 경험을 부드럽게 만든다.
예제
<Suspense fallback={<div>Loading content...</div>}>
<LazyComponent />
</Suspense>tsx정리된 장점
-
초기 로드 속도 향상
초기 페이지에서 필요한 코드만 로드하므로 페이지 로딩 속도가 빨라진다.
-
데이터 사용량 절감
필요한 시점에만 데이터를 로드하므로 사용자의 네트워크 사용량이 줄어든다.
-
사용자 경험 개선
빠른 로딩과 상호작용으로 유저 이탈률을 줄이고 만족도를 높일 수 있다.
이를 통해 우리의 애플리케이션은 불필요한 리소스 낭비를 줄이고, 더 나은 성능과 사용자 경험을 제공할 수 있다.
Code Splitting 도입 결과
실제로 현재 운영하고 있는 서비스에서 Code Splitting 작업을 통해,
기존: 1276.99 kB
현재: 934.81 kB
약 342kB의 초기 다운로드되는 패키지 크기를 줄일 수 있었다.
여담으로 빠른 네트워크에서는
300kB의 패키지 크기를 줄인 것이 엄청난 차이는 아닐 수 있지만, 3G 속도(약 1Mbps 기준)로는300kB를 다운로드하는 데 약 2.4초가 걸린다. 인터넷 환경이 좋지 않은 사람들이 현재 서비스를 이용할 때 이 시간은 사용자에게 느린 초기 로딩으로 다가올 수 있다.
Before
After
Code Splitting 시 유용한 방법
1. TreeShaking
라이브러리 전체 중 일부만 사용한다면 부분 설치를 하자.
lodash는 자바스크립트의 정말 인기 있는 라이브러리 중 하나다. 배열이나 컬렉션, 날짜 등 데이터의 필수적인 구조를 쉽게 다룰 수 있게 하는데 사용된다.
그래서 lodash의 경우 정말 유용한 기능들이 많이 내장되어 있어서 패키지의 크기가 크다. 하지만 현재 서비스에서 만약에 lodash의 debounce만 사용하는데 정말 lodash 패키지 전체를 설치할 필요가 있을까?
우리는 이를 트리 쉐이킹이라고 부른다. ‘필요한 것만 가져다 쓴다’라고 생각하면 된다.
아래 예시 코드를 봐볼까?
pnpm install lodashtsx// lodash에서 debounce 가져오기
import _ from 'lodash';
const handleResize = _.debounce(() => {
console.log('Window resized');
}, 300);
window.addEventListener('resize', handleResize);tsx위 코드에서는 lodash의 전체 패키지를 설치하고 debounce를 사용한다. 그러나 이는 lodash의 모든 유틸리티 함수가 번들에 포함되어 번들 크기가 불필요하게 커지는 결과를 초래한다.
트리 쉐이킹을 사용하면 lodash의 라이브러리 중 사용하는 일부만 설치하여 전체 패키지를 설치하는 것이 아닌 필요한 일부만 설치하여 서버의 리소스가 낭비되는 것을 막을 수 있다.
pnpm install lodash.debouncetsx// lodash.debounce만 가져오기
import debounce from 'lodash.debounce';
const handleResize = debounce(() => {
console.log('Window resized');
}, 300);
window.addEventListener('resize', handleResize);tsx위 코드는 lodash.debounce만을 설치하여 필요한 기능만 번들에 포함한다. 이는 트리 쉐이킹의 일환으로 코드 최적화에 유리하다.
rollup-plugin-visualizer을 사용한 번들 시각화
내가 사용하고 있는 번들의 크기를 빌드했을 때 체크할 수 있다. vite 이전에 WebPack Bundle Analyzer와 유사한 역할을 한다.
pnpm install --save-dev rollup-plugin-visualizertsx해당 파일 설치 후 vite.config.ts에 아래와 같이 설정을 추가로 해주면 된다.
// 추가
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
// 추가
visualizer({
filename: './dist/report.html',
open: true,
brotliSize: true,
}),
svgr(),
],
build: {
sourcemap: true,
},
});tsxpnpm run buildtsx빌드 이후에 바로 아래와 같은 사진이 나오게 된다.
라이브러리의 번들 크기가 너무 크고 혹시라도 대체제가 있다면 다른 대체 라이브러리를 설치하는 것으로 웹 애플리케이션의 빌드 시간을 개선할 수도 있고, 위에서 설명한 다양한 기법들로도 이를 개선할 수 있다.
핵심 정리
- Code Splitting의 본질:
- 애플리케이션 코드를 여러 작은 파일(chunk)로 분리
- 초기 로딩에 필요한 코드만 로드하고 나머지는 나중에
- Lazy Loading 방식으로 필요한 시점에 동적 로딩
- 실제 효과:
- 1276.99kB → 934.81kB (약 342kB 절감)
- 3G 환경에서 약 2.4초 로딩 시간 단축
- 비로그인 사용자는 6개 페이지 코드만 받음 (전체 40개 중)
- 구현 방법:
React.lazy(): 컴포넌트를 렌더링 시점까지 로드 지연<Suspense>: 로딩 중 fallback UI 표시- Webpack/Vite: 자동으로 chunk 분할 및 최적화
- Tree Shaking:
- 필요한 코드만 가져다 쓰기
lodash전체 대신lodash.debounce만 설치- 번들에 사용하지 않는 코드 포함 방지
- 번들 분석 도구:
rollup-plugin-visualizer: 번들 크기 시각화- 어떤 라이브러리가 용량을 많이 차지하는지 파악
- 대체 라이브러리 검토 및 최적화 방향 결정
- 성능 개선 효과:
- 초기 로드 속도 향상
- 데이터 사용량 절감
- 사용자 이탈률 감소
- 느린 네트워크 환경에서 더 큰 체감 개선
Code Splitting은 여행 짐 싸기와 같다. 2주 여행에 옷장을 통째로 가져가지 않고 필요한 옷만 선별해서 가방에 넣는다. 초기 로딩은 기내 수하물(필수품만), 나머지는 위탁 수하물처럼 나중에 받으면 된다. Tree Shaking은 같은 옷이 여러 벌 있을 때 하나만 챙기는 것과 같다.