프론트엔드 라이브러리로 리액트가 많이 사용되면서 SPA(Single Page Application)이 모던웹에서 많이 사용되고 있습니다. SPA 를 사용하면 하나의 자바스크립트로 하여금 html파일에 필요한 domd을 그리게 할 수 있고, 라우팅 또는 유저 인터랙션을 하나의 자바스크립트 파일안에서 모두 해결할 수 있습니다.
이렇게 되면 관리가 쉽고, 좀 더 자연스러운 유저경험을 제공할 수 있지만 (마치 앱과 비슷하게) 빠르게 첫 페이지를 보여줘야 하는 페이지 혹은 네트워크 상황이 좋지 않다면 이런 점은 어느 정도 장애물이 될 수 있는데요.
이런 점을 해결하기 위해서 자주 사용되는 번들러 webpack
은 코드를 분할하는 방법을 제공하고 있고, 리액트 라이브러리에서 또한 Dynamic Import를 통한 code splitting
을 지원합니다.
node_modules는 보통 vendor라는 이름으로 분리 됩니다. vendor를 분리 하기 전 SPA은 위 사진처럼 하나의 js로 실행됩니다.
하지만 이렇게 webpack설정
을 바꿔 무거운 js파일을 나눌 수 있습니다. 링크
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom|lodash-es|lottie-web|date-fns)[\\/]/,
name: 'vendor',
chunks: 'all',
filename: `[name].[chunkhash].js`,
},
},
},
},
정규식을 통해 따로 분리 할 번들파일을 별도로 생성했습니다.
이렇게 될 경우 무거운 파일을 분리할 수 있다는 장점 외에 이점이 있습니다.
위에서 vendor 번들파일을 분리 할때 [name].[chunkhash].js
라는 이름으로 분리 했는데요.
여기서 [chunkhash]
는 node_modules이 바뀌지 않은 이상. 즉, 새로운 패키지를 설치하거나 업데이트 하지 않는 이상 빌드할 때마다 같은 이름으로 유지됩니다. 이렇게 되면 브라우저 캐시의 도움을 얻어 매 배포시마다 전체파일을 다운받지 않고 변경되는 main번들만 다운 받을 수 있습니다.
큰 사이드 이팩트 없이 적용해 볼 수 있는 방법이 하나 더 있습니다.
리액트에서는 다음과 같이 모듈이 실제로 필요한 부분에서 로드 될수 있도록 코드 분할 방법을 제공하는데요
Before
import { add } from './math';
console.log(add(16, 26));
After
import("./math").then(math => {
console.log(math.add(16, 26));
});
Before
import OtherComponent from './OtherComponent';
After
const OtherComponent = React.lazy(() => import('./OtherComponent'));
아래처럼 기존의 Route 구현방식을(react-router v6)
import Home from './pages/Home'
import About from './pages/About'
import Posts from './pages/Posts'
<Routes>
<Route path="/" element={<Home />} />
<Route path="posts" element={<Posts />} />
<Route path="about" element={<About />} />
</Routes>
다음처럼 변경하고 어떤 차이점이 발생했는지 살펴보겠습니다.
const Home = React.lazy(() => import('./pages/Home'))
const About = React.lazy(() => import('./pages/About'))
const Posts = React.lazy(() => import('./pages/Posts'))
<Suspense fallback={<div>loading!</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="posts" element={<Posts />} />
<Route path="about" element={<About />} />
</Routes>
</Suspense>
컴포넌트를 레이지 로딩 하지 않았을 때 vendor를 분리한 청크파일은 이렇게 생성됩니다.
asset bundle.6db81df76b61896aca9a.js 727 KiB [emitted] [immutable] (name: main)
하지만 위에서 원하는 결과대로 빌드가 되면 다음처럼 여러개의 js파일이 생성됩니다
asset bundle.8a2af5e79c6a5a895b15.js 393 KiB [emitted] [immutable] (name: main)
asset src_pages_About_js.bundle.8a2af5e79c6a5a895b15.js 82.4 KiB [emitted] [immutable]
asset src_pages_Posts_js.bundle.8a2af5e79c6a5a895b15.js 3.69 KiB [emitted] [immutable]
asset src_pages_Home_js.bundle.8a2af5e79c6a5a895b15.js 2.6 KiB [emitted] [immutable]
이렇게 된다면 SPA에서 가지고 있는 모든 페이지 소스를 첫 로드시 가져오지 않고 각 페이지가 실제로 route될때 js파일을 다운받습니다.
파일을 비동기적으로 불러오기 때문에 컴포넌트가 렌더링 되기 전에 그려져야 할 Fallback을 처리해주기 위해 Suspense로 분리 되어야 할 페이지 또는 컴포넌트를 감싸줍니다.
<Suspense fallback={<LoadingPage />}>
{...}
</Suspense>
FYI: SSR 을 지원하기 위해서 Loadable Components를 사용할 수 있습니다.
번들사이즈는 웹 성능에 많은 영향을 미칩니다. 단순히 청크를 나누는 일로도 웹 성능 개선에 영향을 미칠 수 있지만 번들 자체 용량을 줄이거나 작게 유지 하는것이 더 좋은 방향이겠죠? :D
번들을 나누기 전에 아래와 같이 번들을 줄일 수 있는 여지가 있는지 먼저 체크 해보시길 바랍니다.
import _ from "lodash"
와 같이 tree-shaking을 하지 않고 해당 라이브러리를 통쨰로 불러오고 있다. 참고: 트리 쉐이킹으로 자바스크립트 페이로드 줄이기-TOAST UIproduction build
, TerserWebpackPlugin
, UglifyJS
와 같은 최적화 도구를 적절하게 사용해서 번들사이즈를 최소화 시키고 있는지 확인한다.