async 함수 내부에서 await import 를 사용하기 위해서는 esm 이 필수적이기 때문에, 기존 commonJS로 작성된 코드를 esm으로 전환 할 필요가 있다.
tsconfig.json수정
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"baseUrl": "./src",
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*", "src/declare.d.ts"],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}
moule을 ESNext로 바꿔준 뒤, ts-node 속성에 esm: true
, experimentalSpecifierResoultion: node
를 추가 해 준다
register-hooks.js 작성
//register-hooks.js
import { register } from "module";
register("ts-node/esm", import.meta.url);
ts-node를 register 한 뒤에 ts-node가 실행되기 때문에, typescript가 아닌 javascript로 작성해 준다.
package.json script 수정
NODE_OPTIONS로 register hooks 를 import 한 뒤 webpack을 실행,
// package.json
...
"scripts": {
"build": "NODE_OPTIONS=\"--import ./register-hooks.js ts-node/esm\" webpack --config ./webpack/webpack.prod.config.ts",
"dev": "NODE_OPTIONS=\"--import ./register-hooks.js ts-node/esm\" webpack serve --config ./webpack/webpack.dev.config.ts"
},
...
package.json 파일에서 type: module
추가
"type": "module"
__dirname
에서 import.meta.url
로 변경
__dirname
과 다르게 import.meta.url
은 파일 이름까지 반환(file:://some/routes/somfile.js)하기 때문에, path.resolve(__dirname, "../dist")
에서 path.resolve(fileURLtoPath(import.meta.url), "../../dist"
로 변경해 준다.
// webpack.common.config.ts
import path from "path";
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
import { fileURLToPath } from "url";
const commonConfig: Configuration = {
entry: "./src/index.ts",
output: {
path: path.resolve(fileURLToPath(import.meta.url), "../../dist"),
filename: "main.js",
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.html$/,
loader: "html-loader",
},
{
test: /\.(png|jpg|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
},
},
generator: {
filename: "./images/[name][ext]",
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
filename: "index.html",
}),
new CleanWebpackPlugin(),
],
};
export default commonConfig;
bootstrap의 modal을 이용해서 todo를 삭제할 때 confirm 모듈을 작성
bootstrap 설치 후 import
yarn add bootstrap; yarn add -D @types/bootstrap
관련 scss 파일 import
// style.scss
@import "../node_modules/bootstrap/scss/bootstrap.scss";
...
modal 컴포넌트 작성
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Todo App</title>
</head>
<body>
<div class="custom-container">
<img
class="header-image"
src="../images/header-image.jpg"
alt="To Do List"
/>
<h1>To Do List</h1>
<div class="todolist-wrapper">
<input class="new-todo" placeholder="Enter text here" autofocus />
<ul class="todo-list"></ul>
</div>
</div>
<div class="modal fade" id="modal-delete-todo">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<p>삭제하시겠습니까?</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
id="modal-dismiss-button"
data-bs-dismiss="modal"
data-bs-target="#my-modal"
aria-label="Close"
>
아니요
</button>
<button
type="button"
class="btn btn-primary"
id="modal-delete-button"
>
네
</button>
</div>
</div>
</div>
</div>
</body>
</html>
관련 함수 작성
// index.ts
const removeTodoEventHandler = async (event: Event) => {
const deleteButton = document.getElementById("modal-delete-button");
const id = getTodoId(event.target as HTMLElement);
deleteButton!.dataset.id = id.toString();
const { Modal } = await import("bootstrap");
const deleteTodoModal = Modal.getOrCreateInstance(
document.getElementById("modal-delete-todo")!
);
deleteTodoModal.show();
};
const confirmRemoveEventHandler = async (event: Event) => {
const id = getTodoId(event.target as HTMLElement);
removeTodo(id!);
renderTodos(getAllTodos());
const { Modal } = await import("bootstrap");
const deleteTodoModal = Modal.getInstance(
document.getElementById("modal-delete-todo")!
);
deleteTodoModal!.hide();
};
document.addEventListener("click", (event: Event) => {
if ((event.target as HTMLElement).classList.contains("delete")) {
removeTodoEventHandler(event);
}
if ((event.target as HTMLElement).classList.contains("real-checkbox")) {
toggleTodoEventListener(event);
}
if ((event.target as HTMLButtonElement).id === "modal-delete-button") {
confirmRemoveEventHandler(event);
}
});
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
main (302 KiB)
css/main.6c55668a03dd.css
js/main.9b46e42e5df4.js
build
를 해 보면 생성되는 번들 사이즈가 크다고 경고가 뜨는데, bundle code 를 split 해 줄 필요가 있다. 어떤 패키지가 추출되어야 할 지 결정하기 위해서 bundle-analyzer를 사용한다
webpack-bundle-analyzer
패키지 설치
yarn add -D webpack-bundle-analyzer @types/webpack-bundle-analyzer
build config과 거의 동일하게 사용해야 하므로, webpack.analyze.config.ts
파일을 따로 만든 후 scripts 에 추가해 주기로 한다
config 작성
// webpack.analyze.config.ts
import prodConfig from "./webpack.prod.config";
import { merge } from "webpack-merge";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
const analyzeConfig = merge(prodConfig, {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "server",
openAnalyzer: false, // 새 창에서 analyzer를 열어주는 역할
analyzerPort: 8888, // port 지정, default = 8888
}),
],
});
export default analyzeConfig;
script 추가
"scripts": {
"build": "NODE_OPTIONS=\"--import ./register-hooks.js ts-node/esm\" webpack --config ./webpack/webpack.prod.config.ts",
"dev": "NODE_OPTIONS=\"--import ./register-hooks.js ts-node/esm\" webpack serve --config ./webpack/webpack.dev.config.ts",
"bundle-analyzer": "NODE_OPTIONS=\"--import ./register-hooks.js ts-node/esm\" webpack --config ./webpack/webpack.analyze.config.ts"
},
optimization.runtimeChunk 도큐먼트
optimization.splitChunks 도큐먼트
번들을 분리하게 되면, 번들 별로 로딩 시점이 다를 수 있는데, 이를 도와주는 역할을 하는게 runtimeChunk 이다, runtime chunk(bundle)을 참고해서 특정 번들을 로딩하거나 pending 시킨다 splitChunk를 이용해서 번들을 분리하기 전에 runtimeChunk: singe
을 적용한 뒤 분리를 해 주면 된다.
splitChunks.chunks = all | inital | async
1. all 은 동기 비동기 상관없이 분리를 하겠다는 의미
2. initial은 비동기일때 사용
3. async는 동기식일때 사용
node_module 번들만 따로 추출해 보자
prodConfig 작성
const prodConfig: Configuration = merge(commonConfig, {
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxSize: Infinity,
minSize: 2000,
cacheGroups: {
node_modules: {
test: /[\\/]node_modules[\\/]/,
name: "node_modules",
chunks: "initial",
}
}
}
}
})
chunks: async
와 name
함수를 이용해서 lazy loading 을 이용하는 패키지를 따로 분리 해 본다
prodConfig 작성
const prodConfig: Configuration = merge(commonConfig, {
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxSize: Infinity,
minSize: 2000,
cacheGroups: {
node_modules: {
test: /[\\/]node_modules[\\/]/,
name: "node_modules",
chunks: "initial",
},
async: {
test: /[\\/]node_modules[\\/]/,
chunks: "async",
name(module: Module, chunks: Array<Chunk>) {
return chunks.map((chunk: Chunk) => chunk.name).join("-");
},
},
}
}
}
})
lazy loading으로 import된 패키지에 webpackChunkName 추가
// index.ts
const removeTodoEventHandler = async (event: Event) => {
const deleteButton = document.getElementById("modal-delete-button");
const id = getTodoId(event.target as HTMLElement);
deleteButton!.dataset.id = id.toString();
const { Modal } = await import(
/* webpackChunkName: "bootstrap" */
"bootstrap"
);
const deleteTodoModal = Modal.getOrCreateInstance(
document.getElementById("modal-delete-todo")!
);
deleteTodoModal.show();
};
const confirmRemoveEventHandler = async (event: Event) => {
const id = getTodoId(event.target as HTMLElement);
removeTodo(id!);
renderTodos(getAllTodos());
const { Modal } = await import(
/* webpackChunkName: "bootstrap" */
"bootstrap"
);
const deleteTodoModal = Modal.getInstance(
document.getElementById("modal-delete-todo")!
);
deleteTodoModal!.hide();
};