지금은 Webpack으로 구축한 React서버에 Code-spliting을 적용해 보겠습니다.
일단 코드부터 보시겠습니다.
Code-spliting
// src/Root.js
import React, { Suspense, lazy, useState } from "react";
import "./style.scss";
import { Route, Routes } from "react-router-dom";
import Menu from "./components/Menu";
const RedPage = lazy(() => import("./pages/RedPage"));
const BluePage = lazy(() => import("./pages/BluePage"));
const Root = () => {
const [clicked, setClickced] = useState(false);
const onButtonClicked = async () => {
const { default: alert } = await import("./alert");
alert();
};
return (
<div>
<input type="button" onClick={onButtonClicked} value="alert" />
<Menu />
<hr />
<Suspense fallback={<div>Loading ...</div>}>
<Routes>
<Route path="/red" element={<RedPage />} />
<Route path="/blue" element={<BluePage />} />
</Routes>
</Suspense>
</div>
);
};
export default Root;
// src/components/Blue.js
import React from "react";
import "../styles/blue.css";
const Blue = () => {
return <div className="Blue">Blue</div>;
};
export default Blue;
// src/components/Red.js
import React from "react";
import "../styles/red.css";
const Red = () => {
return <div className="Red">Red</div>;
};
export default Red;
// src/components/Menu.js
import React from "react";
import { Link } from "react-router-dom";
const Menu = () => {
return (
<div>
<ul>
<li>
<Link to="/red">To RedPage</Link>
</li>
<li>
<Link to="/blue">To BluePage</Link>
</li>
</ul>
</div>
);
};
export default Menu;
// src/pages/BluePage.js
import React from "react";
import Blue from "../components/Blue";
const BluePage = () => {
return (
<div>
<Blue />
</div>
);
};
export default BluePage;
// src/pages/RedPage.js
import React from "react";
import Red from "../components/Red";
const RedPage = () => {
return (
<div>
<Red />
</div>
);
};
export default RedPage;
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import Root from "./Root";
import "@babel/polyfill";
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(
<Router>
<Root />
</Router>,
document.getElementById("root")
);
일단 간략하게 CodeSpliting에 대해 소개하겠습니다.
Code-Spliting
싱글페에지 웹앱의 단점 중 하나가 초기 로딩 때 해당 웹앱의 모든 것을 다 불러와야 한다는 것입니다. 페이스북이나 트위터같은 큰 앱을 처음 불러올 때 모든 걸 다 불러온다면 어마어마한 로딩 시간이 걸리겠죠? 이용자가 모든 페이지를 이용하는게 아님에도 쓸데없는 페이지까지 한 번에 다 불러와야 합니다. 보통 로딩 시간이 3초가 넘어가면 이용자가 접속하길 포기한다고 합니다.
따라서 느린 초기 로딩시간을 극복하고자 ( 큰 bundle-size 한계를 극복하고자 ) 나온 것이 웹팩의 Code-Spliting입니다. 싱글페이지 앱이더라도 해당 라우트를 방문했을 때만 관련된 모듈들을 로딩하도록 하는 겁니다. 내부 구현은 모듈이 알아서 해줄겁니다~
코드 스플리팅을 하는 방법은 크게 두 가지 입니다.
< require.ensure >
require.ensure(['dependency'], function(require) {
const module = require('path or module');
}, 'chunk name')
입니다 여러분의 코드 아무 곳에나 위와 같이 require.ensure로 시작하는 함수를 호츨하면 됩니다. 세 번쨰 인자로 넣어 준 청크 이름을 가진 청크가 하나 생성됩니다. 청크란 하나의 덩어리라는 뜻으로, 코드 스플리팅 시 생성되는 자바스크립트 파일 조각을 의미합니다.
< Dynamic Import >
이 방식은 웹팩2부터 도입된 방식입니다. 이를 쓰기 위해서는 먼저 @babel/plugin-syntax-dynamic-import를 설치하고, babel-loader의 plugins에 넣어주어야 합니다.
// .babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-syntax-dynamic-import"],
}
기본적인 사용방법은 아래와 같습니다.
import(/* webpackChunkName: "청크 이름" */ 'react-filepicker').then(function(filepicker) {
filepicker.init();
}).catch(function(err) {
console.error('filepicker error', err);
});
다시 돌아와서
Root.js파일을 다시 보면
const onButtonClicked = async () => {
const { default: alert } = await import("./alert");
alert();
};
이 보일 것이다 ( 또한 alert파일은 다음과 같다 )
// src/alert.js
module.exports = () => {
alert("click 하셨네요!");
};
이렇게 한다면 Root에서 onButtonClicked을 input의 핸들러로 삽입해주면 button을 클릭하면 Dynamic Import를 활용하여 서버에서부터 파일을 불러와 비동기적으로 사용할 수 있게 되는 것이다.
결과를 보자 ( development mode )
( production mode >> npm run build )
production 모드에서 http://localhost/build/223.~~~.js를 불러오는 것을 볼 수 있다.
chunkFile관리
이제 output에 Dynamic Import를 했으니까 이에 해당되는 chunkFile이 만들어 질 것이다.
// config/webpack.config.prod.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const webpack = require("webpack");
module.exports = {
entry: {
app: "./src/index.js",
},
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "..", "build"),
},
mode: "production",
optimization: {
runtimeChunk: {
name: "runtime",
},
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
name: "vendor",
enforce: true,
},
},
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: "/node_modules",
use: ["babel-loader"],
},
{
test: /\.html$/,
use: [
{
// html파일을 읽었을 떄 html-loader를 실행하여 웹팩이 이해할 수 있게 한다.
loader: "html-loader",
options: { minimize: true },
},
],
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html", // public/index.html 파일을 읽는다.
filename: "index.html",
}),
// 웹팩에서 제공하는 DefinePlugin은 모든 자바스크립트 코드에서 접근이 가능한 전역 변수를 선언하기 위해서 사용되는 플러그인입니다.
// 현재 개발환경이 "development"임을 명시
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development"),
}),
new MiniCssExtractPlugin({
filename: "style-test.css",
}),
new WebpackManifestPlugin({
fileName: "assets.json",
}),
new CleanWebpackPlugin(),
],
};
으로 작성하였는데 output에 filename을 [name].[chunkhash].js로 설정해 주었다.
여기서 [chunk]라는 것이 있는데 이는 웹팩의 빌드타임마다 바뀌는 값입니다. 하지만 빌드마다 모든 파일에 고유해시값이 공통적으로 부여되기 때문에 app.js의 내용은 바뀌었는데 chunkFile의 내용은 안 바뀐 경우에도 새로운 청크이름.고유해시값.js를 불러오게 됩니다. 한 파일만 바뀌어도 나머지 청크들도 다 새로 불러오게 되는 겁니다.
그래서 존재하는 것이 [chunkhash]입니다. 즉 app.hash1.js와 청크이름.hash1.js이 생성되는 겁니다. 즉 이 말은 무엇이냐면 청크이름.hash1.js는 기존 청크해시 값을 그대로 사용하기 때문에 캐싱 시 이득을 볼 수 있습니다.
Vendor를 통한 chunkhash문제 해결
chunk나 chunkhash를 써서 이제 새로 문제가 등장하였습니다. 바로 app.js를 쓰다가 청크해시를 준 이후부터 app.청크해시.js를 사용해야 하는데요. 문제는 청크해시 부분이 어떻게 나올지 미리 예측할 수가 없다는 겁니다. <script src="app.청크해시.js"></script>를 할 때 청크해시 부분에 뭐를 넣어줘야할지 모르는 겁니다. 그래서 나온것이 manifest입니다.
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
module.exports = {
plugin: [
...,
new WebpackManifestPlugin({
fileName: "assets.json",
}),
};
위와 같이 작성해 줍니다. 그럼
// build/assets.json
{
"app.js": "auto/app.4696141ff9e7623894ca.js",
"runtime.js": "auto/runtime.bad6af6ece248616c253.js",
"413.style-test.css": "auto/413.style-test.css",
"413.js": "auto/413.c23b8c4196e951e8df80.js",
"399.style-test.css": "auto/399.style-test.css",
"399.js": "auto/399.4c388fcb38dda4c8454e.js",
"223.js": "auto/223.523711c98b1b8d275518.js",
"vendor.css": "auto/style-test.css",
"vendor.js": "auto/vendor.d647e137454725ac94ac.js",
"index.html": "auto/index.html"
}
와같이 모든 chunkhash가 적용된 js파일들의 이름을 가져올 수 있게 됩니다.
SplitChunk
하지만 chunk, bundle마다 공통적인 모듈을 볼러올 수가 있습니다. 예를들어 A청크가 (a, b, c)패키지를 가지고 있고, B청크가 (a, b, d)패키지를 가지고 있다면, a와 b패키지가 겹치기 때문에 두 번 불러와서 쓸데없는 용량을 잡아먹습니다. 이런 것은 vendor~A~B(a, b)로 만들어주고, A청크는(c), B청크는 (d)로 만들어 중복을 최소화 해줍니다.
// config/webpack.config.prod.js
module.exports = {
...,
optimization: {
runtimeChunk: {
name: "runtime",
},
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
name: "vendor",
enforce: true,
},
},
},
},
},
와 같이 webpack설정 파일에 optimization을 통해서 vendor파일을 만들고 bundle파일의 용량을 최소화할 수 있습니다.
React.Lazy
또한 아까 Dynamic Import는 React Component를 수행하지 못한다는 단점이 있습니다. 그래서 React.lazy와 React.Suspense를 불러와서 아래와 같이 라우팅 부분을 Suspense로 감싸고 라우팅에 해당하는 컴포넌트는 lazy함수 안에 콜백 함수를 작성하여 Dynamic하게 import를 해옵니다.
// src/Root.js
const RedPage = lazy(() => import("./pages/RedPage"));
const BluePage = lazy(() => import("./pages/BluePage"));
const Root = () => {
...
return (
...
<Suspense fallback={<div>Loading ...</div>}>
<Routes>
<Route path="/red" element={<RedPage />} />
<Route path="/blue" element={<BluePage />} />
</Routes>
</Suspense>
);
}
그럼 해당 RedPage와 BluePage컴포넌트를 LazyLoading 즉 Dynamic Import 즉 처음에 다 불러오지 않고 필요할 때만 불러오게 설정할 수 있는 겁니다!
'React > ReactJs' 카테고리의 다른 글
SPA기반 SSR 구현하기 - 2 (0) | 2022.02.19 |
---|---|
SPA기반 SSR구현하기 - 1 (0) | 2022.02.19 |
Webpack을 통한 React개발환경 구축하기 (0) | 2022.02.19 |
useMemo, useCallback, ( React.memo ) (0) | 2022.02.18 |
Webpack 설정 (0) | 2022.02.17 |