webpack을 이용한 lazy-loading 적용하기

joepasss·2023년 10월 31일
0

webpack

목록 보기
7/7
post-thumbnail

cjs 에서 esm으로 전환하기


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 를 추가 해 준다

ts-node options

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;

전체 코드 보기

lazy loading 적용하기


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);
  }
});

생성되는 bundle 사이즈 확인을 위한 bundle-analyzer 설치


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"
  },

node_module 번들 코드 분리하기


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",
				}
			}
		}
	}
})

lazy-loading된 패키지 번들 분리하기


chunks: asyncname 함수를 이용해서 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();
};

전체 코드 보기

0개의 댓글