코드 분할
애플리케이션의 전체 코드를 하나의 번들 파일로 만드는 것은 좋은 생각이 아닐 수 있습니다. 불필요한 코드까지 전송되어 사용자의 요청으로부터 페이지가 렌더링되기까지 오랜 시간이 걸릴 수 있기 때문입니다.(번들 파일을 하나만 만들면 관리 부담이 적어지므로 회사 내부 직원용 애플리케이션을 만들 때는 좋은 선택이 될 수 있습니다.) 많은 수의 사용자를 대상으로 하는 서비스라면 응답 시간을 최소화기 위해 코드를 분할하는 것이 좋습니다.
프로젝트 생성
mkdir webpack-split cd webpack-split npm init -y npm install webpack webpack-cli react lodash
코드를 분할하는 가장 직관적인 방법은 웹팩의 entry 설정값에 페이지별로 파일을 입력하는 것입니다.
src/index1.js
import { Component } from 'react'; import { fill } from 'lodash'; import { add } from './util'; const result = fill([1, 2, 3], add(10, 20)); console.log('this is index1', { result, Component });
src/index2.js
import { Component } from 'react'; import { fill } from 'lodash'; import { add } from './util'; const result = fill([1, 2, 3], add(10, 20)); console.log('this is index2', { result, Component });
두 파일 모두 같은 종류의 모듈을 사용하고 있습니다.
src/util.js
export function add(a, b) { console.log('this is function'); return a + b; }
npm install clean-webpack-plugin
프로젝트 루트의 webpack.config.js 페이지별로 entry 설정
const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: { // (1) page1: './src/index1.js', page2: './src/index2.js', }, output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), }, plugins: [ new CleanWebpackPlugin() ], // (2) mode: 'production', };
- 각 페이지의 js 파일을 entry로 입력합니다.
- dist 폴더를 정리하기 위해 clean-webpack-plugin을 사용합니다.
웹팩을 실행해 보면 page1.js, page2.js 두 파일이 생성됩니다. 하지만 두 파일 모두 같은 모듈의 내용을 포함하고 있기 때문에 비효율적입니다.
SplitChunksPlugin
웹팩에서는 코드 분할을 위해 기본적으로 SplitChunksPlugin을 내장하고 있습니다. 별도의 패키지를 설치하지 않고 설정 파일을 조금 수정하는 것만으로 코드 분할을 할 수 있습니다.
SplitChunksPlugin을 사용하도록 webpack.config.js 파일 수정
// ... module.exports = { entry: { page1: './src/index1.js', }, // ... optimization: { splitChunks: { // (1) chunks: 'all', // (2) name: 'vendor', }, }, // ... };
optimization의splitChunks속성을 이용하면 코드를 분할할 수 있습니다.chunks속성의 기본값은 동적 임포트만 분할하는async입니다. 우리는 동적 임포트가 아니더라도 코드가 분할되도록 all 로 설정합니다.
이 상태로 웹팩을 빌드하면 lodash와 react 모듈은 vendor.js파일로 만들어집니다. util.js 모듈은 파일의 크기가 작기 때문에 page1.js 파일에 포함됩니다.
splitChunks 속성을 제대로 이해하기 위해서는 먼저 기본값의 형태를 이해해야 합니다.
splitChunks 속성의 기본값
module.exports = { // ... optimization: { splitChunks: { chunks: 'async', // (1) minSize: 30000, // (2) minChunks: 1, // (3) // ... cacheGroups: { // (4) default: { minChunks: 2, // (5) priority: -20, reuseExistingChunk: true, }, defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: -10, // ... (모든 괄호 닫기)
- 동적 임포트만 코드를 분할하도록 설정되어 있습니다.
- 파일 크기가 30kb이상인 모듈만 분할 대상으로 합니다.
- 한 개 이상의 청크에 포함되어 있어야 합니다. 청크는 웹팩에서 내부적으로 사용되는 용어로 대개 번들 파일이라고 이해해도 괜찮습니다.
- 파일 분할은 그룹별로 이뤄집니다. 기본적으로 외부 모듈(vendors)과 내부 모듈(default) 두 그룹으로 설정되어 있습니다. 외부 모듈은 내부 모듈보다 비교적 낮은 비율로 코드가 변경되기 때문에 브라우저에 오래 캐싱될 수 있다는 장점이 있습니다.
- 내부 모듈은 두 개 이상의 번들 파일에 포함되어야 분할됩니다.
util.js 모듈을 내부 모듈 그룹으로 분할되도록 설정하기
// ... module.exports = { // ... optimization: { splitChunks: { chunks: 'all', minSize: 10, // (1) cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: 2, name: 'vendors', } default: { minChunks: 2, priority: -20, reuseExistingChunk: true, }, defaultVendors: { minChunks: 1, // (2) priority: 1, name: 'default', // ... (모든 괄호 닫기)
- 파일 크기 제한에 걸리지 않도록 낮은 값을 설정합니다.
- 청크 개수 제한을 최소 한 개로 설정합니다.
이 상태로 웹팩을 실행하면 page1.js, vendors.js, default.js 세 개의 번들 파일이 생성됩니다. util.js 모듈은 default.js 번들 파일에 포함됩니다.
새로운 그룹을 추가해서 리액트 패키지만 별도의 번들 파일로 분할해보겠습니다.
리액트 패키지는 별도로 분할되도록 설정하기
module.exports = { // ... optimization: { splitChunks: { chunks: 'all', minSize: 10, // (1) cacheGroups: { defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: 1, name: 'vendors', }, reactBundle: { test: /[\\/]node_modules[\\/](react | react-dom)[\\/]/, name: 'react.bundle', priority: 2, // (1) minSize: 100, // ... (모든 괄호 닫기)
- 이 그룹의 우선순위가 높아야 리액트 모듈이 vendors 그룹에 들어가지 않습니다.
위와 같이 설정하면 리액트 패키지는 react.bundle.js 파일로 분할됩니다.
동적 임포트
동적 임포트는 동적으로 모듈을 가져올 수 있는 기능입니다. 웹팩에서 동적 임포트를 사용하면 해당 모듈의 코드는 자동으로 분할되며, 오래된 브라우저에서도 잘 동작합니다.
src/index3.js
function myFunc() { import('./util').then(({ add }) => import('lodash').then(({ default: _ }) => console.log('value', _.fill([1, 2, 3], add(10, 20))), ), ); } myFunc();
import 함수는 프로미스 객체를 반환하기 때문에 then 메소드로 연결할 수 있습니다.
index3.js 파일 번들링을 위해 webpack.config.js 파일 수정
const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: { page3: './src/index3.js', }, output: { filename: '[name].js', chunkFilename: '[name].chunk.js', // (1) path: path.resolve(__dirname, 'dist'), }, plugins: [ new CleanWebpackPlugin() ], mode: 'production', };
chunkFilename 속성을 이용해서 동적 임포트로 만들어지는 번들 파일의 이름을 설정합니다.
웹팩을 실행하면 page3.js, 1.chunk.js, 2.chunk.js 3 파일이 생성됩니다. 두 청크 파일에는 util.js 모듈과 lodash 모듈의 코드가 들어갑니다. 참고로 웹팩 런타임 코드는 page3.js 파일에만 들어갑니다.
<html>
<body>
<script type="text/javascript" src="./page3.js"></script>
</body>
</html>
page3.js 파일의 script 태그만 만들어도 됩니다. page3.js 파일이 생성되면서 동적으로 나머지 두 파일을 가져옵니다.
두 모듈을 동시에 가져오는 코드
async function myFunc() { const [{ add }, { default: _ }] = await Promise.all([ import('./util'), import('lodash'), ]); console.log('value', _.filee([1, 2, 3], add(30, 20))); } myFunc();
Promise.all을 이용하여 두 모듈을 동시에 가져옵니다.
분할된 파일을 prefetch, preload로 빠르게 가져오기
만약 myFunc 함수가 버튼의 이벤트 처리 함수로 사용된다면 버튼을 클릭하기 전에는 두 모듈을 가져오지 않습니다. 이는 꼭 필요할 때만 모듈을 가져오기 때문에 lazy loading으로 불립니다. 게으른 로딩은 번들 파일의 크기가 큰 경우에는 응답 속도가 느리다는 단점이 있습니다.
웹팩에서는 동적 임포트를 사용할 때 HTML의 prefetch, preload 기능을 활용할 수 있도록 옵션을 제공합니다. prefetch는 가까운 미래에 필요한 파일이라고 브라우저에게 알려 주는 기능입니다. HTML에서 prefetch로 설정된 파일은 브라우저가 바쁘지 않을 때 미리 다운로드됩니다. 따라서 prefetch는 게으른 로딩의 단점을 보완해줄 수 있습니다.
preload는 지금 당장 필요한 파일이라고 브라우저에게 알리는 기능입니다. HTML에서 preload로 설정된 파일은 첫 페이지 로딩 시 즉시 다운로드됩니다. 따라서 preload를 남발하면 첫 페이지 로딩 속도에 부정적인 영향을 줄 수 있으므로 주의해야 합니다.
prefetch, preload 설정하기
await new Promise(res => setTimeout(res, 1000)); // (1) const [{ add }, { default: _ }] = await Promise.all([ import(/*webpackPreload: true*/ './util'), import(/*webpackPrefetch: true*/ 'lodash'), ]); // ...
- 너무 빠르게 처리하면 prefetch 효과를 확인할 수 없으므로 1초 기다립니다.
- util.js 모듈은 preload로 설정합니다.
- lodash 모듈은 prefetch로 설정합니다.
웹팩 실행 후 브라우저에서 결과를 확인해봅니다.
<html>
<head>
<link rel="prefetch" as="script" href="1.chunk.js">
<script charset="utf-8" src="1.chunk.js"></script>
<script charset="utf-8" src="2.chunk.js"></script>
</head>
<body>
<script type="text/javascript" src="./page3.js"></script>
</body>
</html>
1.chunk.js 파일은 prefetch가 적용됐습니다. link 태그는 page3.js 파일이 실행되면서 웹팩에 의해서 삽입됩니다. script 태그도 myFunc 함수가 실행될 때 웹팩에 의해서 삽입됩니다.
그런데 이상한 점이 있습니다. 분명 preload 기능을 이용한다고 했는데, preload설정이 HTML코드가 반영되지 않은 것입니다. 사실 preload는 첫 페이지 요청 시 전달된 HTML 태그 안에 미리 설정되어 있어야 하므로 웹팩이 지원할 수 있는 기능은 아닙니다. 대신 웹팩은 page3.js 파일이 평가될 때 2.chunk.js 파일을 즉시 다운로드함으로써 어느 정도는 preload 기능을 흉내 낼 수 있습니다.