[TIL 17] - [JS] Webpack의 loader와 plugin

찐새·2023년 6월 25일
0

TIL

목록 보기
16/20
post-thumbnail
post-custom-banner

Inflearn - 프론트엔드 개발환경의 이해와 실습을 수강하며 정리한 내용입니다.
웹팩 버전은 다음과 같습니다.

node @16
webpack @4
webpack-cli @3

1. 로더

웹팩은 로더를 사용하여 모든 파일을 모듈로 취급한다. 이 덕분에 JS뿐만 아니라 CSS, TS 등도 JS에서 로드할 수 있다.

1-1. 커스텀 로더

로더를 직접 만들어 사용할 수 있다. 로더 함수를 가진 파일을 만든 후 webpack.config.js에 추가한다.

// custom-loader.js
module.exports = function myCustomLoader(content) {
  console.log("custom-loader runs");
  return content;
};

// webpack.config.js
const path = require("path");

module.exports = {
  // (...)
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [path.resolve("./custom-loader.js")],
      },
    ],
  },
};

실행될 로더는 module 속성 내에 작성한다. 여기서는 .js로 끝나는 모든 파일에 한해서 custom-loader.js가 동작한다. 빌드를 실행하면 결과를 볼 수 있다.

> webpack

custom-loader runs
custom-loader runs

위에서 math.jsapp.js 파일을 작성했기 때문에 custom-loader가 두 번 동작하는 것을 볼 수 있다.

1-2. 자주 사용하는 로더

  1. css-loader

css-loader를 사용하면 CSS 파일을 import로 호출할 수 있다.

npm install css-loader@3으로 설치한다. 강의에 따라 webpack@4 버전을 사용하기 때문에 호환되는 버전으로 맞췄다.

webpack.config에 css-loader를 추가한다.

module.exports = {
  // (...)
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["css-loader"],
      },
    ],
  },
};

웹팩은 .css 파일을 만나면 css-loader를 실행한다.

[./src/app.css] 277 bytes {main} [built]
[./src/app.js] 87 bytes {main} [built]
[./src/math.js] 360 bytes {main} [built]

main.js에 CSS 코드가 추가되었지만, 브라우저에는 반영되지 않는다. HTML이 DOM으로 변환되어야 렌더링되듯이, CSS 역시 CSSOM이 되어야 화면에 나타나기 때문이다. 이를 반영하기 위해서 style-loader를 함께 사용한다.

  1. style-loader

강의 버전에 맞춰 npm install style-loader@1로 설치한다. 로더를 config의 module에 추가하는데, css-loader보다 앞에 추가한다. 로더가 여러 개일 때는 뒤에서부터 앞으로 실행되기 때문이다.

module.exports = {
  // (...)
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

빌드해 보니 스타일이 적용되었다.

apply style loader

  1. file-loader

이미지 같은 파일들도 로더를 사용하여 모듈로 호출할 수 있다.

body {
  background-image: url(bg.png);
}

CSS에서 파일로 가져온 이미지를 모듈화하기 위해 npm install file-loader@5 하고, 모듈에 추가한다.

module.exports = {
  // (...)
  module: {
    rules: [
      {
        // (...)
      },
      {
        test: /\.png$/,
        use: ["file-loader"],
      },
    ],
  },
};

빌드는 성공하지만 배경화면이 적용되지는 않는다. index.html 입장에서는 같은 폴더가 아닌 dist에 이미지가 있기 때문이다. 해결하기 위해 옵션을 추가한다.

module.exports = {
  // (...)
  module: {
    rules: [
      {
        // (...)
      },
      {
        test: /\.png$/,
        loader: "file-loader",
        options: {
          publicPath: "./dist/",
          name: "[name].[ext]?[hash]",
        },
      },
    ],
  },
};

options에 추가한 부분를 보면, publicPath는 빌드 후 파일이 있을 경로이고, name은 파일의 이름을 설정하는 역할이다. 순서대로 [파일명].[확장자]?[해시]이며, 해시는 캐시 무력화를 위해 사용한다.

해시가 없다면 브라우저는 파일이 바뀌어도 이름이 같다면 저장된 캐시를 사용한다. 때문에 빌드마다 해시를 변경하여 파일이 바뀌었음을 브라우저에 알린다.

  1. url-loader

이미지가 많으면 네트워크 자원에 사용에 부담이 생기고, 사이트 성능에 악영향을 끼칠 수 있다. 만약 한 페이지에 작은 이미지를 여러 개 사용한다면, 이미지를 Base64로 인코딩하여 소스로 넣는 Data URI Scheme 방법이 나을 수도 있다. url-loader는 이러한 방식을 돕는다.

먼저 app.jsimg 태그에 이미지 파일을 모듈로 불러와 소스에 넣는다.

import "./app.css";
import nyancat from "./nyancat.jpg";

document.addEventListener("DOMContentLoaded", () => {
  document.body.innerHTML = `
  <img src="${nyancat}" />`;
});

npm install url-loader@3으로 설치하고 로더를 config에 추가한다.

module.exports = {
  // (...)
  module: {
    rules: [
      {
        // (...)
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: "url-loader",
        options: {
          publicPath: "./dist/",
          name: "[name].[ext]?[hash]",
          limit: 20000, // 20kb
        },
      },
    ],
  },
};

file-loader와 유사하지만 용량의 limit을 지정하여 빌드 방식을 구분할 수 있다. limit: 20000으로 설정했으니 20kb 이하의 이미지는 base64로 인코딩되어 main.js에 들어가고, 그보다 큰 이미지는 file-loader가 처리하여 위의 배경 이미지처럼 동작한다.

2. 플러그인

플러그인은 번들된 결과물을 난독화하거나 특정 텍스트를 추출하는 용도이다.

2-1. 커스텀 플러그인

플러그인은 로더와 다르게 class로 정의한다.

// custom-plugin.js

class CustomPlugin {
  apply(compiler) {
    compiler.hooks.done.tap("Custom Plugin", (stats) => {
      console.log("CustomPlugin: done");
    });
  }
}

module.exports = CustomPlugin;

플러그인은 plugins 속성의 배열에 넣는다.

// webpack.config.js
const path = require("path");
const customPlugin = require("./custom-plugin");

module.exports = {
  // (...)
  plugins: [new customPlugin()],
};

빌드를 실행하면 플러그인에 적은 대로 출력되는 것이 보인다.

> webpack

CustomPlugin: done

다음은 빌드마다 빌드 날짜를 남기는 플러그인이다.

class CustomPlugin {
  apply(compiler) {
    const date = new Date();
    compiler.plugin("emit", (compilation, callback) => {
      const source = compilation.assets["main.js"].source();

      compilation.assets["main.js"].source = () => {
        const banner = [
          "/**",
          " * 이것은 BannerPlugin이 처리한 결과입니다.",
          ` * Build Date: ${date.getFullYear()}${
            date.getMonth() + 1
          }${date.getDate()}`,
          " */",
        ].join("\n");
        return banner + "\n\n" + source;
      };

      callback();
    });
  }
}

module.exports = CustomPlugin;

웹팩의 Banner Webpack Plugin을 참고했다. compilation에서 main.js에 해당하는 소스를 받고, 빌드 날짜를 기록하는 코드와 소스를 합친다. main.js를 보면 소스 코드 제일 앞에 배너가 적혀 있는 것을 볼 수 있다.

// dist/main.js

/**
 * 이것은 BannerPlugin이 처리한 결과입니다.
 * Build Date: 2023년 6월 25일
 */

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// (...)

2-2. 자주 사용하는 플러그인

  1. BannerPlugin

웹팩에서 기본적으로 제공하며, 빌드 결과물에 빌드 정보나 커밋 정보 등을 추가할 수 있는 플러그인이다.

// webpack.config.js
const Webpack = require("webpack");
const childProcess = require("child_process");

module.exports = {
  // (...)
  plugins: [
    new Webpack.BannerPlugin({
      banner: `
        Build Date: ${new Date().toLocaleString()}
        Commit Version: ${childProcess.execSync("git rev-parse --short HEAD")}
        Author: ${childProcess.execSync("git config user.name")}
      `,
    }),
  ],
};

// 결과
/*!
 *
 *         Build Date: 2023. 6. 25. 오후 11:48:01
 *         Commit Version: 0a18573
 *
 *         Author: Real-Bird
 *
 *
 */
/******/ (function (modules) {
/******/ // The module cache
/******/ var installedModules = {};
/******/ // (...)

child_process는 터미널 명령어를 실행해주는 노드 모듈이다. 그것을 이용해 현재 커밋하는 깃의 버전(git rev-parse --short HEAD)과 작성자(git config user.name)를 적어 빌드한 결과물이다.

  1. DefinePlugin

개발환경과 운영환경에 따라 의존적인 환경 변수를 다루기 위해 사용하는 플러그인이다.

// webpack.config.js
const Webpack = require("webpack");
const childProcess = require("child_process");

module.exports = {
  // (...)
  plugins: [
    new Webpack.DefinePlugin({
      TWO: "1+1",
    }),
  ],
};

// app.js
console.log(TWO);

플러그인에서 정의한 TWO는 전역으로 사용할 수 있는 코드이며, 빌드 후 실행했을 때 콘솔에 2가 찍힌다. 만약 코드가 아닌 문자열을 출력하고 싶다면 JSON.stringify로 감싸 문자열로 변환한다.

  1. HtmlTemplatePlugin

써드파티 플러그인으로, HTML 파일도 웹팩 빌드 과정에 포함할 때 사용한다. npm install html-webpack-plugin@3으로 설치한다.

HTML을 엔트리 포인트가 있는 소스 폴더로 이동시키고 내부의 스크립트를 지운다.

// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // (...)
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
  ],
};

빌드하면 ./dist/index.html이 포함되어 있고, 스크립트가 추가된 것을 확인할 수 있다.

esj문법을 이용하여 웹팩 빌드에서 HTML의 정보를 변경할 수 있다.

<title>Document<%= env %></title>

타이틀에 <%= env %>를 추가한다.

// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // (...)
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      templateParameters: {
        env: process.env.NODE_ENV === "development" ? "(개발용)" : "",
      },
    }),
  ],
};

templateParameters에 HTML에 추가했던 env를 설정한다. NODE_ENVdevelopment이면 타이틀에 (개발용)이 붙고, 아니라면 아무것도 붙지 않는다. NODE_ENV=development npm run build로 빌드한다.

<title>Document(개발용)</title>

개발용이 추가된 것이 보인다.

문서의 주석도 제거할 수 있다. minify 옵션에 필요한 설정을 추가한다.

// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // (...)
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      templateParameters: {
        env: process.env.NODE_ENV === "development" ? "(개발용)" : "",
      },
      minify: {
        collapseWhitespace: true,
        removeComments: true,
      },
    }),
  ],
};

collapseWhitespace는 공백을 제거하고, removeComments는 주석을 제거한다. 이것 역시 환경에 따라 구분하는 것이 좋다. production일 때만 활성화하고 아닐 때는 false로 설정한다.

  1. CleanWebpackPlugin

써드파티 플러그인이며, 아웃풋 폴더의 이전 빌드 내용을 삭제해 준다. npm install clean-webpack-plugin@3으로 설치한다.

// webpack.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  // (...)
  plugins: [new CleanWebpackPlugin()],
};

테스트를 위해 ./dist/ 폴더에 foo.js를 추가하고 빌드를 실행해 보면 결과물에 foo.js가 사라졌음을 확인할 수 있다.

  1. MiniCssExtractPlugin

써드파티 플러그인이며, 스타일 코드만 뽑아서 CSS 파일로 만들어 준다. 코드를 여러 파일로 나누는 게 좋은데, 브라우저에서는 하나의 큰 파일을 다운로드하는 것보다 여러 개의 작은 파일을 동시에 다운로드하는 것이 더 빠르기 때문이다.

npm install mini-css-extract-plugin@0으로 설치한다.

// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  // (...)
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          process.env.NODE_ENV === "production"
            ? MiniCssExtractPlugin.loader
            : "style-loader",
          "css-loader",
        ],
      },
    ],
  },
  plugins: [
    ...(process.env.NODE_ENV === "production"
      ? [new MiniCssExtractPlugin({ filename: "[name].css" })]
      : []),
  ],
};

production이 아닌 환경에서는 빌드 과정을 줄이는 것이 나으므로 추가 조건을 설정한다. MiniCssExtractPlugin을 사용할 경우에는 전용 로더를 사용하는 것이 나으므로, module의 스타일 코드도 수정한다.

실행을 확인하기 위해 NODE_ENV=production npm run build로 빌드한다. 결과물을 보면 이전에 인라인 스타일로 포함되었던 것이 main.css로 생성된 것을 볼 수 있다.

profile
프론트엔드 개발자가 되고 싶다
post-custom-banner

0개의 댓글