Webpack을 이용한 코드 스플리팅
코드 스플리팅은 webpack같은 모듈 번들러들의 중요한 역할 중 하나다. 하나의 큰 번들을 여러개의 작은 번들들로 쪼개준다. 왜 하는지 또 어떻게 적용하는지 알아보자.
코드 스플리팅
코드 스플리팅은 하나의 큰 번들을 여러개의 작은 번들들로 쪼개준다. 이를 잘 사용하면 필요할 때 필요한 번들만 로드 함으로써 초기 로딩시간을 줄여주고, 유저가 현재 필요하지 않은 코드는 로드 하지 않음으로써 앱의 성능도 크게 향상 시킬 수 있다.
또, 앱에 비해 잘 바뀌지 않는 third-party 라이브러리를 하나의 번들로 따로 묶어둠으로서 앱을 바꿔도 유저가 스크립트를 다시 다운받을 필요 없게(캐싱된 코드를 사용하면 되므로) 해준다.
웹팩을 이용하여 이런식으로 간단한 코드 스플리팅을 할 수 있다. (webpack 공식 문서 예제)
const path = require("path");
module.exports = {
mode: "development",
entry: {
index: "./src/index.js",
another: "./src/another-module.js"
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist")
}
};
하나의 번들이 아닌 index.bundle.js와 another.bundle.js로 쪼개는 방식이다. 만일 번들의 수가 너무 많을 경우 script 태그에 하나하나 집어넣는게 쉽지 않은데, 이럴 때는 html-webpack-plugin을 이용하자.
이러한 방식에는 한계가 있다.
먼저, index.js에서 어떤 라이브러리를 사용하고 있고, another-module.js에서 같은 라이브러리를 사용하고 있다고 생각해보자. 그러면 그 라이브러리는 index.bundle.js와 another.bundle.js 각각에 한 번씩 총 두 번 추가될 것이다. 즉, 코드가 중복된다.
이 문제는 webpack의 optimization 옵션을 이용하여 해결 할 수 있다.
const path = require("path");
module.exports = {
mode: "development",
entry: {
index: "./src/index.js",
another: "./src/another-module.js"
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist")
},
optimization: {
splitChunks: {
chunks: "all"
}
}
};
이렇게 옵션을 주면, 중복되는 그 라이브러리를 index와 another에서 제거하고 다른 하나의 번들로 만들어 준다.
가장 간단한 코드 스플리팅이고 이런 방식으로 third-party 라이브러리를 따로 번들해주면 도움이 될 것이다.
하지만, 이러한 방법으로는 코드 스플리팅의 가장 큰 장점인 필요할 때 로드하는 것이 힘들다. 이 때 사용할 수 있는 방법이 dynamic import이다.
Dynamic import
dynamic import는 말그대로 동적으로 import 할 수 있게 해주는 구문이다. (글 작성시 기준으로 tc39의 stage-3에 있다.) webpack은 이를 이용한 코드 스플리팅을 지원해준다. 이를 사용하기 위해서는 webpack에서 entry가 아닌 chunk파일의 이름을 정해주는 chunkFilename 옵션만 지정해주면 된다.
const path = require("path");
module.exports = {
mode: "development",
entry: {
index: "./src/index.js"
},
output: {
filename: "[name].bundle.js",
chunkFilename: "[name].bundle.js",
path: path.resolve(__dirname, "dist")
}
};
그리고 dynamic import를 사용하기 위해 babel의 @babel/plugin-syntax-dynamic-import 플러그인을 설정해야한다. (여기서는 따로 설정하는 방법을 다루지는 않겠다.)
잘 동작하는지 알아보기 위해 다음과 같은 코드를 작성해보자.
export default function Hello() {
const element = document.createElement("div");
element.innerText = "Hello!";
return element;
}
hello.js는 Hello! 라는 텍스트를 가진 div 엘리먼트를 반환하는 함수를 export 해주는 파일이다.
function sayHello() {
import(/* webpackChunkName: "hello" */ "./hello").then(({ default: Hello }) =>
document.body.appendChild(Hello())
);
}
function Button() {
const element = document.createElement("button");
element.innerText = "Say hello!";
element.onclick = sayHello;
return element;
}
document.body.appendChild(Button());
버튼을 누르면 hello.js 파일에서 엘리먼트를 받아서 body에 붙여주는 코드이다. sayHello를 보면, hello.js에서 default를 Hello라는 이름으로 동적으로 받은 후, body에 붙여준다.
dynamic import라는 말에 맞게 import는 비동기적으로 이루어져 promise를 반환한다. export default로 반환된 값을 resolve 값으로 받아서 사용할 수 있다.
주석처리 해준 /* webpackChunkName: "hello" */는 hello.js 파일의 chunk 이름을 hello.bundle.js로 해주는 옵션이다.
이제 webpack을 통해 build 해보자.
npx webpack
파일 이름이 webpack.config.js 일 경우 webpack이 알아서 찾아준다. 그렇지 않을 경우 --config [파일이름] 옵션을 주어 실행시켜주면 된다.
[이미지 2]
다음과 같은 결과가 나올 것이다. 그러면 마지막으로 index.html파일을 생성한 후 index.bundle.js만 삽입해주자.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Webpack App</title>
</head>
<body>
<script type="text/javascript" src="index.bundle.js"></script>
</body>
</html>
그리고 index.html을 열어 네트워크 탭을 통해 쪼개놓은 코드가 우리가 필요할 때 로드되는지 확인해보자. 버튼을 클릭하기 전에는 index.bundle.js 스크립트가 로드되고, 버튼 클릭 후에 hello.bundle.js가 로드된다면 잘 적용된 것이다.
이렇게 dynamic import를 이용한 code splitting을 알아보았다.
React에서 code splitting
추가적으로 React에서 code splitting을 하는 방법을 알아보자.
위와 같은 방법으로 설정하면 되고, create-react-app을 이용한 경우에는 webpack 설정이 다 되어있기 때문에 바로 dynamic import를 사용 가능하다. 원래 loadable-components를 사용했으나, React 16.6 부터는 lazy와 Suspense를 이용하여 코드 스플리팅을 할 수 있다. (아직 ssr에서는 지원되지 않는다.)
먼저 간편하게 create-react-app을 통해 프로젝트를 만들어 보자.
npx create-react-app code-splitting
cd code-splitting
아까와 같이 버튼을 클릭하면 Hello.js를 로드하는 것을 구현해보자.
import React from "react";
export default function Hello() {
return <div>Hello!</div>;
}
import React, { Component, lazy, Suspense } from "react";
import "./App.css";
const Hello = lazy(() => import("./Hello"));
class App extends Component {
state = {
showHello: false
};
render() {
const { showHello } = this.state;
return (
<div className="App">
<button onClick={() => this.setState({ showHello: true })}>
Click me!
</button>
<Suspense fallback={<div>loading...</div>}>
{showHello && <Hello />}
</Suspense>
</div>
);
}
}
export default App;
그리고 앱을 실행하자.
yarn start
아까와 같이 네트워크 탭에서 버튼을 클릭했을 때, 로드가 일어나는 것을 확인할 수 있다!