webpack5 설정하기

Maliethy·2022년 3월 27일
0

webpack

목록 보기
2/2

webpack 설정을 위해 찾아본 내용들을 정리하고자 한다.

1. webpack-dev-server 설정하기

devServer의 번들된 결과물은 일반 webpack 명령어와는 다르게 메모리에 저장된다. 따라서 개발 서버를 종료하면 결과물도 사라진다.
출처: 웹팩 데브 서버(webpack-dev-server) 3버전(feat. proxy)

webpack devserver 옵션 중 static 설정은 정적파일을 제공하는 경우 필요한 경로이다.
출처: Webpack 공식문서-devserver.static

나의 경우 html파일들과 js, css, img 폴더에 있는 모든 파일들이 정적파일들이므로 다음과 같이 'dist'에서 해당 정적파일들을 찾으면 된다고 설정했다.

       static: [
        {
          directory: path.resolve(__dirname, "/dist"),
        },
      ],

빌드했을 때 빌드된 파일들은 path 설정에 따라 'dist'폴더에 생성된다. publicPath가 빈값이므로 빌드한 파일의 url은 'http://127.0.0.1:5500/dist/index.html'와 같이 'dist' path 뒤에 다른 경로가 추가되지 않았다.

    output: {
      path: path.resolve(__dirname, "dist"),
      publicPath: "",
      filename: "js/[name].[chunkhash].js",
      chunkFilename: "js/[name].chunk.js",
      assetModuleFilename: "img/[name][ext]",
    },

chunkFileName 설정은 따로 청크파일 이름 구성을 정해주려면 chunkFilename 옵션을 사용하는데 사용된다. 기본 설정은 [id]는 청크 순서대로 0,1,2,3,...을 부여한다. 여기서 청크(chunk)란 하나의 덩어리라는 뜻으로, 코드 스플리팅 시 생성되는 자바스크립트 파일 조각을 의미한다.
출처: 웹팩5로 청크 관리 및 코드 스플리팅 하기

assetModuleFilename 설정은 다음 ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot 확장자로 끝나는 resource들의 경로를 설정해준다.

        {
          test: /\.(ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
          type: "asset/resource",
        },

2. css파일과 background url 경로 설정하기

먼저 css module의 rule에서 development모드와 production모드를 구분해서 loader를 지정해준다.

      {
          test: /\.(css|s[ac]ss)$/i,
          use: [
            prod
              ? {
                  loader: MiniCssExtractPlugin.loader,
                  options: {
                       publicPath: "../",
                  },
                }
              : "style-loader",
            "css-loader",
            {
              loader: "sass-loader",
              options: {
                implementation: require.resolve("sass"),
              },
            },
          ],
        },

development모드에서는 css에 대한 지속적인 수정이 이루어지기 때문에 style-loader를 사용한다.
production모드에서는 다음과 같이 MiniCssExtractPlugin를 사용한다. MiniCssExtractPlugin가 빌드 전 js에서 import한 css 파일을 빌드 후에는 js파일과 분리해 html에 link 태그 형태로 해당 css파일을 넣어준다.

/src/js/index.js

import "../css/index.css";
import("./common.js")
  .then(({ default: common }) => {
    common();
  })
  .catch((err) => {
    console.error("common error", err);
  });

/dist/index.html

<Head>
    <script
      defer="defer"
      src="js/index.860ef49ae25452bc0eea.js?ea4a9077a3de00c3236e"
    ></script>
    <link
      href="css/index.860ef49ae25452bc0eea.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
</Head>    

MiniCssExtractPlugin의 옵션 중 filename은 빌드 후 css 파일의 경로와 이름을 설정할 수 있다.
publicPath 옵션은 모든 css 파일의 background url 경로에 '../'를 추가해준다.

빌드 전에는 다음과 같이 ("img/main_banner.png")처럼 상대경로로 설정되어 있다.

.main_banner div {
  width: 880px;
  margin: auto;
  height: 100%;
  background: url("img/main_banner.png") 50% calc(100% - 40px) no-repeat;
  background-size: contain;
}

빌드 후 css파일의 background url 경로에는 다음과 같이 설정된다.

참고: WEBPACK 이미지 경로 처리

3.성능 개선을 위해 코드 줄이기와 코드 분할하기

출처: Dynamic Import 로 웹페이지 성능 올리기
참고:
Webpack을 이용한 코드 스플리팅

모든 웹 페이지의 속도를 결정 짓는 첫번째 요소는 첫 페이지를 그릴때 필요한 자원양이다.
필요한 자원이 많으면 많을 수록 네트워크 상에서 다운받는 시간이 오래걸린다.
Webpack 같은 bundler 들은 모든 JS 코드를 하나의 거대한 파일로 만들어 내었고, 이는 웹페이지에서 페이지를 그릴때 필요한 자원의 양 또한 크게 증가하게 되었다.
때문에 웹사이트의 속도를 올리기 위해서는 큰 JS 파일의 용량을 줄여하는데, 크게 코드의 크기를 줄이는 방법과, 코드를 분할하는 방법이 있다.

(1) 코드 줄이기

코드 줄이기의 방법은 webpack.optimization 중 minimizer 옵션을 이용해 압축하거나 uglify-js 를 이용할 수 있다. 압축한 형태는 다음과 같다.

venders.22ba66c5797265730272.css

index.860ef49ae25452bc0eea.js

(2) 코드 분할하기

웹 사이트는 많은 정보를 담고 있으며, 이를 여러 페이지로 나누어 존재할 확률이 높다. Webpack으로 빌드한 파일은 전체 페이지의 내용을 하나의 파일에 담고 있다. 따라서 이를 모두 렌더링 하더라도, 사용자가 보는 페이지는 얼마 되지 않는다.
이를 여러 페이지에서 필요한 파일들로 나누고, 각 페이지에서 필요한 시점마다 불러올 수 있도록 나눈다면, 첫 렌더링에 필요한 파일의 크기를 줄여 더 빠르게 렌더링 할 수 있다.

  • Webpack 에서는 코드를 분할 하기위해, 목적 별로 여러 Entry로 분할 할 수 있다. 이에 해당하는 설정부분은 다음과 같다.
const entry = {
    load_HTML: path.resolve(__dirname, "src/js/load_HTML.js"),
    load_common: path.resolve(__dirname, "src/js/load_common.js"),
    common: path.resolve(__dirname, "src/js/common.js"),
    review_slider: path.resolve(__dirname, "src/js/review_slider.js"),
    top_banner_slider: path.resolve(__dirname, "src/js/top_banner_slider.js"),
  };
  • 중복된 코드도 분할하여 관리해주는 webpack의 옵션으로는 optimization.splitChunks 이 있다. 청크간에 겹치는 패키지들을 별도의 파일로 추출해주는 옵션으로 아래 설정을 통해 중복된 코드는 venders라는 이름의 파일로 묶어서 제공해준다.
    optimization: {
      usedExports: true,
      minimize: true,
      minimizer,
      runtimeChunk: "single",
      splitChunks: {
        chunks: "all",
        maxInitialRequests: Infinity,
        minSize: 0,
        cacheGroups: {
          vendor: {
            chunks: "initial",
            name: "vendor",
            enforce: true,
          },
        },
      },
    },

optimization.usedExports 설정이 true일 때 사용되지 않는 import는 번들링하지 않는다.
optimization.minimizer에서 minimizer 관련 plugin을 설정할 수 있다.
BundleAnalyzerPlugin은 현재 사용하고 있는 번들크기를 시각화한 treemap을 볼 수 있다. TerserPlugin은 Javascript 코드를 minify/minimize 해준다. CssMinimizerPlugin은 CSS 파일을 minify 해준다.
출처: 빌드 최적화 하기

  • 대부분의 코드들은 사용자가 보는 첫 페이지에는 필요하지 않다.첫 페이지 진입시에 필요한 최소한의 코드만 다운 받고, 사용자가 특정 페이지나 위치에 도달할 때마다 코드를 로드 한다면, 첫 페이지의 초기 성능을 올릴 수 있다.이런 방식을 lazy-load 게으른 로딩이라고 한다. Dynamic Import를 사용하면, 런타임시에 필요한 module 을 import 할 수 있다. Dynamic Import는 다음과 같은 모양으로 js 파일을 import해서 Promise 객체로 반환된 default 매개변수로 해당 js파일에서 모듈을 가져다 쓸 수 있다.
 import("./common.js")
  .then(({ default: common }) => {
    common();
  })
  .catch((err) => {
    console.error("common error", err);
  });

Webpack에서 Dynamic Import를 사용하기 위해서는 babel-plugin-syntax-dynamic-import 플러그인이 필요하다.

/.babelrc

{
  "plugins": ["@babel/plugin-syntax-dynamic-import"],
  "presets": [
    [
      "@babel/preset-env",
      { "targets": { "browsers": ["last 2 versions", ">= 5% in KR"] } }
    ],
    "@babel/typescript"
  ]
}

Build 시점에 import() 모듈을 chunk 파일로 만들며, 필요한 시점에 header 에 script 를 세팅하여 JS 파일을 다운로드 한다.
이때 chunk파일명은 webpack의 output에 세팅한 파일명을 따라간다.

 output: {
  filename: '[name].[chunkhash].js'
}

출처: 웹팩5로 청크 관리 및 코드 스플리팅 하기

나의 경우는 무식하지만 가장 쉬운 방법인 entry에서 각 페이지별로 필요한 js 파일들을 나누어 만들고 import했기 때문에 사실 Dynamic Import 관련 설정이 필요없었다고 생각된다. 그럼에도 이를 적용한 이유는 Webpack HtmlWebpackPlugin chunksSortMode로 실행순서 조절하기에서 다룬 이슈인 공통 js 파일(common.js)와 각 페이지의 하위 메뉴(이번 프로젝트에서는 sub2.js, sub3.ts, sub6.js의 경우가 그랬다) 로드의 순서를 조절하기 위해 사용했다.

4. webpack 전체 설정과 build 전과 후 folder 구조 비교하기

(1) webpack 전체 설정

/src/webpack.config.js

const path = require("path");
const webpack = require("webpack");
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 FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const BundleAnalyzerPlugin =
  require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = (env, options) => {
  const prod = options.mode === "production";

  const htmlPageNames = [
    "index",
    "sub1",
    "sub2",
    "sub2_2", 
    "sub2_3",
    "sub2_4", 
    "sub2_5", 
    "sub3", 
    "sub3_2",
    "sub3_3",
    "sub4", 
    "sub5", 
    "sub6",
    "sub6_2",
    "sub6_2_detail",
    "sub6_3",
    "service_use_term",
    "privacy_info_use_term", 
    "header_nav",
    "footer_float_menu",
    "top_banner",
  ];

  const entry = {
    load_HTML: path.resolve(__dirname, "src/js/load_HTML.js"),
    load_common: path.resolve(__dirname, "src/js/load_common.js"),
    common: path.resolve(__dirname, "src/js/common.js"),
    review_slider: path.resolve(__dirname, "src/js/review_slider.js"),
    top_banner_slider: path.resolve(__dirname, "src/js/top_banner_slider.js"),
  };
  
  const multipleHtmlPlugins = htmlPageNames.map((name, idx) => {
    const splited = name.split("_")[0];
    let chunks = idx < 18 ? ["load_HTML", "top_banner_slider"] : [];

    // console.log("splited", splited);

    if (splited !== "header" && splited !== "footer" && splited !== "top") {
      if (splited === "privacy" || splited === "service") {
        (entry[splited] = path.resolve(__dirname, `src/js/privacy.js`)),
          chunks.push("privacy");
      } else {
        if (splited === "sub3") {
          (entry[splited] = path.resolve(__dirname, `src/js/${splited}.ts`)),
            chunks.push(splited);
        } else {
          (entry[splited] = path.resolve(__dirname, `src/js/${splited}.js`)),
            chunks.push(splited);
        }
      }
    }

    (idx === 3 ||
      idx === 5 ||
      idx === 6 ||
      idx === 7 ||
      idx === 10 ||
      idx === 11) &&
      chunks.push("review_slider");

    // console.log(name, chunks);

    return new HtmlWebpackPlugin({
      template: path.resolve(`src/${name}.html`),
      filename: `${name}.html`,
      chunks,
      hash: true,
      chunksSortMode: "manual",
    });
  });

  const plugins = [
    new webpack.ProvidePlugin({ $: "jquery" }),
    new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
      filename: "css/[name].[chunkhash].css",
    }),
    new WebpackManifestPlugin(),
    new FaviconsWebpackPlugin({
      logo: path.resolve(__dirname, "src/img/modument-logo.png"),
      mode: "light",
      devMode: "light",
      inject: true,
      cache: true,
    }),
  ].concat(multipleHtmlPlugins);

  if (!prod) {
    plugins.push(new webpack.HotModuleReplacementPlugin());
  }

  const minimizer = [new CssMinimizerPlugin(), new TerserPlugin()];
   if (!prod) {
     minimizer.push(new BundleAnalyzerPlugin());
   }

  const config = {
    mode: prod ? "production" : "development",
    devtool: prod ? "eval" : "hidden-source-map",
    resolve: {
      extensions: [".js", ".ts", ".css", ".scss"],
      modules: [path.resolve(__dirname, "src"), "node_modules"],
    },
    entry,
    output: {
      path: path.resolve(__dirname, "dist"),
      publicPath: "",
      filename: "js/[name].[chunkhash].js",
      chunkFilename: "js/[name].chunk.js",
      assetModuleFilename: "img/[name][ext]",
    },
    plugins,
    module: {
      rules: [
        {
          test: /\.(ts|js)$/,
          use: [
            "babel-loader",
            {
              loader: "ts-loader",
            },
          ],
          exclude: path.join(__dirname, "node_modules"),
        },

        {
          test: /\.(ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
          type: "asset/resource",
        },
        {
          test: /\.(css|s[ac]ss)$/i,
          use: [
            prod
              ? {
                  loader: MiniCssExtractPlugin.loader,
                  options: {
                    filename: "css/[name].[chunkhash].css",
                    publicPath: "../",
                  },
                }
              : "style-loader",
            "css-loader",
            {
              loader: "sass-loader",
              options: {
                implementation: require.resolve("sass"),
              },
            },
          ],
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/i,
          type: "asset/resource",
        },
      ],
    },
    devServer: {
      static: [
        {
          directory: path.resolve(__dirname, "/dist"),
        },
      ],
      open: true,
      compress: true,
    },
    optimization: {
      usedExports: true,
      minimize: true,
      minimizer,
      runtimeChunk: "single",
      splitChunks: {
        chunks: "all",
        maxInitialRequests: Infinity,
        minSize: 0,
        cacheGroups: {
          vendor: {
            chunks: "initial",
            name: "vendor",
            enforce: true,
          },
        },
      },
    },
  };

  return config;
};

/src/package.json

{
  "name": "modument_homepage2022",
  "version": "1.0.0",
  "description": "",
  "main": "/index.html",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack serve --env development",
    "start": "webpack --mode production"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/maliethy/modument_homepage2022.git"
  },
  "author": "maliethy",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/maliethy/modument_homepage2022/issues"
  },
  "homepage": "https://github.com/maliethy/modument_homepage2022#readme",
  "devDependencies": {
    "@babel/core": "^7.17.5",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-typescript": "^7.16.7",
    "@types/jquery": "^3.5.14",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "css-loader": "^6.6.0",
    "css-minimizer-webpack-plugin": "^3.4.1",
    "favicons": "^6",
    "favicons-webpack-plugin": "^5.0.2",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.5.3",
    "sass": "^1.49.9",
    "sass-loader": "^12.6.0",
    "style-loader": "^3.3.1",
    "ts-loader": "^9.2.6",
    "typescript": "^4.6.2",
    "webpack": "^5.69.1",
    "webpack-bundle-analyzer": "^4.5.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.7.4",
    "webpack-manifest-plugin": "^4.1.1"
  },
  "dependencies": {
    "core-js": "^3.21.1",
    "jquery": "^3.6.0",
    "regenerator-runtime": "^0.13.9",
    "swiper": "^8.0.6"
  }
}

/src/js/sub2.js

import "../css/sub2.css";

import("./common.js")
  .then(({ default: common }) => {
    common();
    $(function () {
      sub2();
    });
  })
  .catch((err) => {
    console.error("common error", err);
  });

function sub2() {
  const prod = process.env.NODE_ENV === "production";

  if ($(".sub2_menu").hasClass("active") === false) {
    $(".sub2_menu").addClass("active");
    $(".sub3_menu").removeClass("active");
    $(".sub6_menu").removeClass("active");
    for (let i = 2; i < 6; i++) {
      if (window.location.href.split("/")[prod ? 4 : 3] === `sub2_${i}.html`) {
        $(".sub_header > ul > li").removeClass("on");
        $(`.sub_header > ul > li:nth-child(${i})`).addClass("on");
      }
    }
  }
  
  생략...
  }

(2) 빌드 전 폴더구조

(3) 빌드 후 생성된 dist 폴더 구조와 manfest.json, html

/dist/manifest.json

{
  "load_HTML.css": "css/load_HTML.0dee04ab7503e5087798.css",
  "load_HTML.js": "js/load_HTML.0dee04ab7503e5087798.js",
  "load_common.js": "js/load_common.b455fd96e01db80d68b3.js",
  "common.js": "js/common.4b8b1c68e2e0bc8a0c69.js",
  "review_slider.css": "css/review_slider.d2616e5358c99fedfdbf.css",
  "review_slider.js": "js/review_slider.d2616e5358c99fedfdbf.js",
  "top_banner_slider.js": "js/top_banner_slider.b9ed1ac041e6665bd01f.js",
  "index.css": "css/index.860ef49ae25452bc0eea.css",
  "index.js": "js/index.860ef49ae25452bc0eea.js",
  "sub1.css": "css/sub1.c5b54d705d4847039a48.css",
  "sub1.js": "js/sub1.c5b54d705d4847039a48.js",
  "sub2.css": "css/sub2.d32f76861cd271c02f5e.css",
  "sub2.js": "js/sub2.d32f76861cd271c02f5e.js",
  "sub3.css": "css/sub3.02ece1f07edac4beee54.css",
  "sub3.js": "js/sub3.02ece1f07edac4beee54.js",
  "sub4.css": "css/sub4.39f23416de687109ab8f.css",
  "sub4.js": "js/sub4.39f23416de687109ab8f.js",
  "sub5.css": "css/sub5.4d43762ac2dbcc9e13c9.css",
  "sub5.js": "js/sub5.4d43762ac2dbcc9e13c9.js",
  "sub6.css": "css/sub6.a73b488593679a4cf20c.css",
  "sub6.js": "js/sub6.a73b488593679a4cf20c.js",
  "service.js": "js/service.4e45ff3047cf76721d80.js",
  "privacy.js": "js/privacy.d9b50e2d1d8d1e505f7d.js",
  "runtime.js": "js/runtime.c8e2b5046570adef20ac.js",
  "js/283.chunk.js": "js/283.chunk.js",
  "js/199.js": "js/199.d79f0cfd69e9e6602269.js",
  "venders.css": "css/venders.22ba66c5797265730272.css",
  "venders.js": "js/venders.22ba66c5797265730272.js",
  "css/778.css": "css/778.622fc862883010c45312.css",
  "js/778.js": "js/778.622fc862883010c45312.js",
  "img/notice1.jpg": "img/notice1.jpg",
 
   생략... 

}

/dist/index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1"
    />
    <title>MODUMENT</title>
    <link rel="icon" href="assets/favicon.png" />
    <script
      defer="defer"
      src="js/runtime.c8e2b5046570adef20ac.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/venders.22ba66c5797265730272.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/load_HTML.0dee04ab7503e5087798.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/top_banner_slider.b9ed1ac041e6665bd01f.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/index.860ef49ae25452bc0eea.js?ea4a9077a3de00c3236e"
    ></script>
    <link
      href="css/venders.22ba66c5797265730272.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
    <link
      href="css/load_HTML.0dee04ab7503e5087798.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
    <link
      href="css/index.860ef49ae25452bc0eea.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
  </head>
   <body>
    <div class="swiper-container top_banner"></div>
    생략...
    <div id="footer_float_menu"></div>
  </body>
</html>

/dist/sub2.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1"
    />
    <title>MODUMENT</title>
   <link rel="icon" href="assets/favicon.png" />
    <script
      defer="defer"
      src="js/runtime.c8e2b5046570adef20ac.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/venders.22ba66c5797265730272.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/load_HTML.0dee04ab7503e5087798.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/top_banner_slider.b9ed1ac041e6665bd01f.js?ea4a9077a3de00c3236e"
    ></script>
    <script
      defer="defer"
      src="js/sub2.d32f76861cd271c02f5e.js?ea4a9077a3de00c3236e"
    ></script>
    <link
      href="css/venders.22ba66c5797265730272.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
    <link
      href="css/load_HTML.0dee04ab7503e5087798.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
    <link
      href="css/sub2.d32f76861cd271c02f5e.css?ea4a9077a3de00c3236e"
      rel="stylesheet"
    />
  </head>
  <body>
    <div class="swiper-container top_banner"></div>
       생략...
    <div id="footer_float_menu"></div>
  </body>
</html>

profile
바꿀 수 있는 것에 주목하자

0개의 댓글