얼마 전에 본 면접에서 SPA(Single Page Application)
의 단점을 어떻게 해결하느냐에 대한 질문을 받았다. "코드 스플리팅을 통해 파일을 여러 개로 분산하여 로드한다"라고 답했으나, 양심의 가책을 느꼈다. 내 프로젝트에서는 코드를 나눴어도 index.js
의 용량이 어마어마했기 때문이다.
lazy
를 사용하여 컴포넌트 코드는 스플리팅했지만, 중심이 되는 파일의 용량은 매우 컸다. 뭐 이리 뚱뚱한가 봤더니 사용한 라이브러리가 모두 index
에 담겨 있었다. 고민했지만 답은 경고 문구에 있었다.
Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
vite
에서 빌드 도구로 사용하는 rollup
의 수동 스플릿 방법을 안내한다. 공통으로 사용하는 모듈을 분리하는 역할이다. 사용법을 익히기 위해 간단한 프로젝트를 만들어봤다.
npm create vite@latest
를 실행하고 React-js
환경으로 설정했다. 페이지를 스플리팅하기 위해 react-router-dom
도 함께 설치했다.
/src
┣─ App.jsx
├─ About.jsx
└─ main.jsx
대충 이렇게 파일을 만들고 라우트를 작성했다.
import React, { Suspense, lazy } from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import App from "./App";
import About from "./About";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter basename="/">
<nav>
{/* 대충 네비게이션 */}
</nav>
<Routes>
{/* 대충 라우트 */}
</Routes>
</BrowserRouter>
</React.StrictMode>
);
기본적인 방식으로 컴포넌트를 라우트에 등록했다. 이 상태로 여과 없이 빌드해 보자.
빌드 로그를 보면 파일 세 개만 생겼다. 내가 사용한 모듈과 컴포넌트는 모두 index.js
에 들어있을 터이다.
빌드한 파일을 실행해 보면 빌드된 단 하나의 index.js
만 로드한다. 큰 프로젝트가 이렇게 로드된다고 생각해 보자. SPA
의 단점인 초기 로딩 속도 느림이 아주 도드라질 것이다.
이번에는 React.lazy
를 사용해서 스플리팅 후 빌드해보자.
const App = lazy(() => import("./App"));
const About = lazy(() => import("./About"));
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter basename="/">
<nav>
{/* 대충 네비게이션 */}
</nav>
<Routes>
{/* 대충 라우트 */}
</Routes>
</BrowserRouter>
</React.StrictMode>
);
의도한 대로 App
과 About
컴포넌트는 코드가 분리되어 새로운 파일로 생성되었다. 여기까지가 내가 생각한 코드 스플리팅이었는데, 아무리 봐도 저 index.js
의 크기가 납득되지 않았다. 겨우 0.11kb
분리하고 스플리팅이라니. 그렇기 때문에 위에서 언급한 rollupOptions
를 추가한 것이다.
rollupOptions
는 vite.config.js
에서 build
속성에 추가하여 설정한다.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes("node_modules")) {
return `vendor`;
}
},
},
},
},
});
manualChunks
에 추입하는 id
에 뭐가 들었나 궁금해 콘솔을 찍어봤더니 사용한 모듈의 경로가 쭉 나왔다. 이 상태로 빌드하면 node_modules
에서 사용한 코드가 vendor.js
에 모두 모인다. index.js
는 가벼워지겠지만, vendor
가 무거워진다. 라이브러리가 무거운 거야 어쩔 수 없지마는, 조절할 수 있다면 최대한 줄여보는 게 좋겠다 싶었다.
일단 id
에 찍힌 모듈 모양은 이렇다.
D:/work-space/test-build/node_modules/react-dom/index.js?commonjs-module
D:/work-space/test-build/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-proxy
D:/work-space/test-build/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports
...
D:/work-space/test-build/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports
필요한 부분은 node_modules
뒷부분의 모듈명이다. 이것만 잘라낸다.
const module = id.split("node_modules/").pop().split("/")[0];
/*
...
react
react
react
react-dom
scheduler
scheduler
*/
vendor/${module}
을 반환값으로 설정하면 vendor
디렉토리에 모듈별로 파일이 생긴다.
빌드 파일로 앱을 실행해 보면 차이점이 확연히 드러난다.
이런 방법으로 내 프로젝트의 빌드 용량을 최적화하여 690KB에 육박하던 index.js
크기를 약 30KB로 줄였다.
찾은 방법으로 코드를 나누긴 했지만 맞는 방법인지는 모르겠다. 더 나은 방법은 더 찾아봐야겠지만, 소기의 목적은 달성했다. 공부하다 보면 더 나은 방법이 떠오를지도 모르겠다.