웹팩에 대해 알아보자 - Plugin이란?

방구석 코딩쟁이·2024년 2월 1일
0

FE개발환경

목록 보기
4/5
post-thumbnail

플러그인

loader는 파일 단위로 처리해주었지만 plugin은 번들된 결과물을 가지고 처리를 해줍니다.
주로 번들된 자바스크립트 코드를 난독화하거나 특정 텍스트를 추출하는 용도로 사용됩니다.

공식문서에 따르면, 플러그인은 웹팩 생태계에 핵심 요소이며, 웹팩의 compilation 과정을 활용(tap into)할 수 있는 강력한 방법을 제공합니다.
플러그인은 각 컴파일 과정에서 발생하는 주요 입네트에 연결(hook into)할 수 있습니다. 모든 단계에서 플러그인은 compiler에 대한 액세스 권한을 가지며, 해당되는 때의 compilation 과정에 대해서도 권한을 갖습니다.

1) 커스텀 플러그인 만들기

로더와 달리 플러그인은 클래스로 제작하며, 클래스 안에서 apply 함수를 구현하면 됩니다. tap에 들어갈 event hook을 지정하고, 웹팩의 내부 인스턴스 별 데이터를 조작하면 됩니다.

플러그인은 prototypeapply메서드를 가지고 있는 인스턴스화된 object입니다. apply 메서드는 플러그인을 설치하는 동안, 웹팩 컴파일러에 의해 한 번 호출됩니다. 이 apply메서드는 기본 웹팩 컴파일러에 대한 참조를 제공하며, 해당 참조를 통해 compiler callback에 대한 액세스 권한을 부여합니다.

그럼 커스텀 플러그인을 작성해보도록 하죠. plugin 디렉토리를 만들고 MyWebpackPlugin.js 파일을 만든 뒤 아래의 코드를 작성해봅시다.

class MyWebpackPlugin {
  apply(compiler) {
    // Tap into the compiler to access the hooks
    compiler.hooks.done.tap('My Plugin', (stats) => {
      /* stats is passed as an argument when done hook is tapped.  */
      console.log('MyPlugin: done');
    });
  }
}

module.exports = MyWebpackPlugin;
  • 인자로 받은 compiler 객체 안에 있는 tap 함수를 사용하는 코드입니다.
    플러그인 작업이 완료(done)되는 시점에 로그를 찍는 코드입니다.
  • stats 객체는 compilation 프로세스 코드에 대한 정보를 제공합니다.

webpack.config.js에 플러그인을 적용해보도록 합시다.

const MyWebpackPlugin = require("./plugin/MyWebpackPlugin");

module.exports = {
  ..., // 다른 웹팩 옵션들
  plugins: [new MyWebpackPlugin()],
}
  • plugins 속성의 배열에 추가해줍니다.
    클래스로 제공되는 플러그인의 생성자를 실행해서 넘기는 방식입니다.

이제 빌드를 해봅시다.
빌드하게 되면 완료되는 시점에서 MyPlugin: done을 출력하고 있는 것을 볼 수 있습니다.

이번에는 webpack.config.js에서 옵션 객체를 넣을 수 있도록 플러그인을 작성해봅시다.

먼저 플러그인 클래스를 아래와 같이 고쳐봅시다.

class MyWebpackPlugin {
  constructor(options = {}) {
    console.log("MyPlugin: constructor", options);
  }

  apply(compiler) {
    // Tap into the compiler to access the hooks
    compiler.hooks.done.tap("My Plugin", (stats) => {
      /* stats is passed as an argument when done hook is tapped.  */
      console.log("MyPlugin: done");
    });
  }
}

module.exports = MyWebpackPlugin;

그리고 webpack.config.js에 넣은 플러그인에 인자를 넣어 호출해보도록 합시다.

plugins: [new MyWebpackPlugin({ options: true })],

그러고 난 뒤에, 빌드를 실행해보면 done 이전에 생성자 함수 쪽에서 작성했던 console.log가 먼저 실행되었음을 확인해볼 수 있습니다.

이번에는 플러그인을 통해 번들 결과에 접근해보도록 하죠. 커스텀 웹팩 파일을 다음과 같이 수정해봅시다.

class MyWebpackPlugin {
  constructor(options = {}) {
    console.log("MyPlugin: constructor", options);
  }

  apply(compiler) {
    // Tap into the compiler to access the hooks
    compiler.hooks.done.tap("My Plugin", (stats) => {
      /* stats is passed as an argument when done hook is tapped.  */
      console.log("MyPlugin: done");
    });

    compiler.hooks.emit.tapAsync("My Plugin", (compilation, callback) => {
      // Tap into the compilation instance to access the assets
      const source = compilation.assets["main.js"].source();
      console.log(source);
      callback();
    });
  }
}

module.exports = MyWebpackPlugin;

다시 빌드를 해보면 다음과 같이 출력이 되었음을 확인할 수 있습니다.

즉, 커스텀 플러그인의 constructor를 실행하고, emit.tapAsync를 실행한 후에, done 을 차례대로 실행했음을 알 수 있습니다.
또한, source 변수에는 소스코드가 문자열 형태로 들어가 있음을 확인할 수 있었습니다.

플러그인을 통해 출력 결과에 시그니처를 추가하는 실습을 진행해봅시다. 커스텀 웹팩 파일을 다음과 같이 수정해봅시다.
(기존의 방식은 적용이 잘 되지 않았기 때문에, webpack의 소스코드 중 BannerPlugins를 찾아서 적용을 해봤습니다 - 링크)

class MyWebpackPlugin {
  constructor(options = {}) {
    console.log("MyPlugin: constructor", options);
  }

  apply(compiler) {
    // Tap into the compiler to access the hooks
    compiler.hooks.done.tap("My Plugin", (stats) => {
      /* stats is passed as an argument when done hook is tapped.  */
      console.log("MyPlugin: done");
    });

    compiler.hooks.emit.tap("emit", (compilation) => {
      // Tap into the compilation instance to access the assets
      // 번들링된 소스코드
      const source = compilation.assets["main.js"].source();
      // console.log(source);

      compilation.updateAsset("main.js", (old) => {
        return {
          source: () => {
            const banner = [
              "/**",
              " * 이 소스코드는 제가 점령했습니다.",
              " * 이 글을 쓴 사람은 누굴까요?",
              " */",
            ].join("\n");
            return banner + "\n\n" + source;
          },
          size: () => {
            return source.length;
          },
        };
      });
    });
  }
}

module.exports = MyWebpackPlugin;

그리고 나서 다시 빌드를 해볼까요?
빌드하고 난 뒤에 main.js를 보게되면 저희가 작성한 글이 소스코드에 합쳐진 것을 볼 수 있습니다.

2) 많이 사용하는 플러그인들

플러그인은 webpack에서 제공하는 플러그인을 사용하거나 써드 파티 라이브러리를 찾아 사용하게 됩니다.

(1) BannerPlugin

저희가 작성했던 커스텀 플러그인과 비슷한 역할을 해주는 플러그인입니다. 결과물에 빌드 정보나 커밋 버전 같은 것을 추가해줄 수 있습니다. 또한, webpack에서 제공해주는 플러그인이기도 합니다.
먼저 git init으로 깃을 실행하고 git add ., git commit -m "<작성할 메시지>"를 통해 로컬 환경에서 버전을 관리하도록 구현합니다.

그 다음 webpack.config.js의 플러그인 옵션을 추가해줍니다.

const webpack = require("webpack");

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

그 다음 빌드를 진행해봅시다.
다음과 같이 main.jscommit version, commit 작성자, build date가 추가되었음을 확인할 수 있습니다.

(2) DefinePlugin

애플리케이션은 보통 개발환경과 프로덕션 환경을 나눠서 운영하게 됩니다. 이 때, 환경에 따라 API 서버 주소를 다르게 운영할 수 있는데 같은 소스 코드를 두 환경에 배포하기 위해서는 환경 의존적인 정보를 소스가 아닌 곳에서 관리하는 것이 좋습니다. 배포할 때마다 사람이 코드를 수정하는 것은 위험하기 때문입니다.

웹팩은 이러한 환경정보를 제공하기 위해 DefinePlugin을 제공합니다.

webpack.config.jsDefinePlugin을 추가해봅시다.

 plugins: [
   ..., // 다른 플러그인들
   new webpack.DefinePlugin({}),
 ],

DefinePlugin에 빈 객체를 전달해도 코드에 기본적으로 넣어주는 값이 있습니다. 노드의 환경 정보인 process.env.NODE_ENV이며, webpack.config.jsmode 속성의 값이 여기에 들어갑니다. "development"로 설정했기 때문에 애플리케이션 코드에서 process.env.NODE_ENV 변수로 접근하게 되면 "development"라는 값을 반환합니다.

console.log(process.env.NODE_ENV); // "development"

웹팩 컴파일 시간에 결정되는 값을 전역 상수 문자열로 애플리케이션에 주입할 수 있습니다.
플러그인을 다음과 같이 수정해봅시다.

new webpack.DefinePlugin({
  TWO: "1+1",
})
  • TWO라는 전역 변수에 1+1이라는 코드조각을 넣었습니다.

그리고 app.js에 아래와 같이 TWO라는 전역변수에 접근해보도록 합시다.

import * as math from "./math.js";
import "./style.css";
import nyancat from "./assets/images/nyancat.jpg";

const imgElem = document.querySelector("#image");
imgElem.src = nyancat;

console.log('TWO', TWO);

이렇게 작성한 후 빌드한 후에 html을 실행해보면 다음과 같이 2라는 값이 찍혀있음을 알 수 있습니다.

만약 코드가 아닌 값(리터럴)을 넘기려면 stringify한 뒤에 넘기면 됩니다.

먼저 webpack.config.js의 플러그인 설정 중 DefinePlugin을 다음과 같이 수정해봅시다.

new webpack.DefinePlugin({
  VERSION: JSON.stringify("v.1.2.3"),
  PRODUCTION: JSON.stringify(false),
  MAX_COUNT: JSON.stringify(999),
  "api.domain": JSON.stringify("http://dev.api.domain.com"),
})

app.js에서 DefinePlugin에서 넘겨준 전역 변수에 접근해봅시다.

import * as math from "./math.js";
import "./style.css";
import nyancat from "./assets/images/nyancat.jpg";

const imgElem = document.querySelector("#image");
imgElem.src = nyancat;

// console.log(math.sum(1, 2, 3));
// console.log('TWO', TWO);
console.log("VERSION", VERSION);
console.log("PRODUCTION", PRODUCTION);
console.log("MAX_COUNT", MAX_COUNT);
console.log("api.domain", api.domain);

빌드한 후에, html을 실행해보면 다음과 같은 결과를 얻게 됩니다.

빌드 타임에 결정된 값을 어플리케이션에 전달할 때,DefinePlugin을 사용하면 됩니다.

(3) HtmlWebpackPlugin

HtmlWebpackPlugin 플러그인은 써드 파티 패키지입니다. HTML 파일을 후처리하는데 사용됩니다. 이를 통해 빌드 타임의 값을 넣거나 코드를 압축할 수 있습니다.

먼저 패키지를 다운로드합시다.
npm i -D html-webpack-plugin

이 플러그인으로 빌드하면 HTML 파일로 아웃풋에 생성됩니다.

먼저 index.html 파일을 src 폴더 내부로 옮기고 아래와 같이 작성합시다.

// src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>타이틀: <%= env %></title>
</head>
<body>
  <img id="image" style="position: absolute; z-index: 100;"></img>
  <!-- <script src="./dist/main.js"></script> -->
  <!-- <script src="./src/app.js"></script> -->
  <!-- <script type="module" src="./src/app.js"></script> -->
</body>
</html>
  • 타이틀 부분에 ejs 문법을 이용했으며, <%= env %>는 전달받은 env 변수 값을 출력합니다.
  • HtmlWebpackPlugin은 이 변수에 데이터를 주입시켜 동적으로 HTML 코드를 생성합니다.
    뿐만 아니라 웹팩으로 빌드한 결과물을 자동으로 로딩하는 코드를 주입해줍니다. (이 때문에 스크립트 로딩 코드를 주석처리했습니다)

webpack.config.jsHtmlWebpackPlugin 플러그인을 추가해줍시다.

plugins: [
  new HtmlWebpackPlugin({
    template: "./src/index.html", // 템플릿 경로를 지정
    templateParameters: { // 템플릿에 주입할 파라미터 변수 지정
      env: process.env.NODE_ENV === "development" ? "(개발용)" : "",
    },
  })
],

이렇게 설정을 하게 된다면 NODE_ENV=development로 설정해서 빌드하면 빌드결과가 타이틀: (개발용)으로 나오고 NODE_ENV=production으로 설정해서 빌드하면 빌드결과가 타이틀: 로 나옵니다.

먼저 NODE_ENV=development npm run build 명령어로 빌드해봅시다.
그럼 다음과 같이 index.html이 생성됩니다.

<title>을 확인해보면 타이틀: (개발용)이 생겼음을 확인할 수 있습니다.
또한, <script defer src="main.js"></script>도 추가되었습니다.

이번에는 NODE_ENV=production npm run build 명령어로 빌드해봅시다.
그럼 다음과 같이 index.html이 생성됩니다.

이번에는 production 환경에서는 파일을 압축하고, 불필요한 주석을 제거하는 옵션을 추가해봅시다.

먼저 webpack.config.jsHtmlWebpackPlugin을 다음과 같이 변경해봅시다.

new HtmlWebpackPlugin({
  template: "./src/index.html", // 템플릿 경로를 지정
  templateParameters: {
    // 템플릿에 주입할 파라미터 변수 지정
    env: process.env.NODE_ENV === "development" ? "(개발용)" : "",
  },
  minify:
  process.env.NODE_ENV === "production"
  ? {
    collapseWhitespace: true, // 빈칸 제거
    removeComments: true, // 주석 제거
  }
  : false,
}),

NODE_ENV=production npm run build 명령어로 빌드를 하면 다음과 같이 빌드됨을 확인할 수 있습니다.

주석도 제거되고, 파일의 공백을 압축시켰음을 볼 수 있습니다.

브라우저 캐시로 인해 정적파일 배포시, 즉각적으로 브라우저에 반영되지 않는 경우가 있어 이를 예방하기 위한 옵션도 존재합니다.

webpack.config.jsHtmlWebpackPlugin을 다음과 같이 변경해봅시다.

 new HtmlWebpackPlugin({
   template: "./src/index.html", // 템플릿 경로를 지정
   templateParameters: {
     // 템플릿에 주입할 파라미터 변수 지정
     env: process.env.NODE_ENV === "development" ? "(개발용)" : "",
   },
   hash: true, // 빌드할 때마다 해시값을 붙여줌
   minify:
   process.env.NODE_ENV === "production"
   ? {
     collapseWhitespace: true, // 빈칸 제거
     removeComments: true, // 주석 제거
   }
   : false,
 }),

그 다음에 npm run build 명령어로 빌드를 해보면 다음과 같이 빌드할 때 생성되는 해시값을 정적파일 로딩 주소의 쿼리스트링으로 붙여서 HTML을 생성합니다.

(4) CleanWebpackPlugin

CleanWebpackPlugin은 빌드 이전 결과물을 제거하는 플러그인입니다. 빌드 결과물은 아웃풋 경로에 모이는데 과거에 빌드했던 파일이 남아있을 수 있습니다. 이러한 현상을 CleanWebpackPlugin을 통해 해결할 수 있습니다.

먼저 패키지를 설치합시다.
npm install -D clean-webpack-plugin

그 다음에 webpack.config.js의 플러그인에 CleanWebpackPlugin을 추가해줍시다.

plugins: [
  ..., // 다른 플러그인들 
  new CleanWebpackPlugin(),
]

이후, 빌드하면 빌드하기 전에 이전 파일들을 삭제한 후에 빌드하게 됩니다.

(5) MiniCssExtractPlugin

CSS 파일이 점점 많아지면 하나의 자바스크립트 결과물로 만들기보다는 번들 결과에서 스타일시트 코드만 뽑아서 별도의 CSS 파일로 만들어 역할에 따라 파일을 분리하는 것이 좋습니다.
왜냐하면 큰 파일 하나를 다운로드 받는 것보다 여러 개의 작은 파일을 동시에 다운로드하는 것이 더 효율적이기 때문입니다.

개발 환경에서는 CSS를 하나의 모듈로 처리해도 상관없지만 프로덕션 환경에서는 분리하는 것이 더 효과적이며, MiniCssExtractPlugin은 CSS를 별도의 파일로 뽑아내는 플러그인이다.

먼저 패키지를 설치해보도록 합시다.
npm install -D mini-css-extract-plugin

그 다음에 webpack.config.js의 플러그인에 MiniCssExtractPlugin을 추가해줍시다.

plugins: [
  ..., // 다른 플러그인들
  ...(process.env.NODE_ENV === "production"
      ? [new MiniCssExtractPlugin({ filename: `[name].css` })] // 프로덕션 환경일 때만 사용
      : [
    function () {
      this.hooks.done.tap("done", (stats) => {
        if (stats.compilation.errors && stats.compilation.errors.length) {
          console.log("Build Error");
          process.exit(1);
        }
      });
    },
  ]),
]
  • production용으로 빌드할 때만 별도의 css 파일을 분리시켜주도록 플러그인을 추가했습니다.
  • filename에 설정한 값으로 아웃풋 경로에 CSS 파일이 생성됩니다.

여태껏 css-loader에 의해 js 모듈로 변경된 스타일시트를 적용하기 위해 style-loader을 사용했었는데, 이제는 production 환경에서는 별도의 CSS 파일로 추출되는 플러그인을 적용했으므로 이를 위한 loader가 필요합니다.
때문에 아래와 같이 production일 때와 아닐 때를 분기처리해서 로더를 추가해봅시다.

module: {
  rules: [
    {
      test: /\.css$/, // .css로 끝나는 파일을 찾아라
      use: [
        process.env.NODE_ENV === "production"
        ? MiniCssExtractPlugin.loader // 프로덕션 환경
        : "style-loader",
        "css-loader",
      ], // css-loader를 사용해라
    },
  ... // 다른 로더 설정
}

이제 NODE_ENV=production npm run build 명령어로 빌드를 해보면 다음과 같이 css는 분리되어 있고, index.html에는 css를 로딩하는 코드가 추가되었습니다.
(난독화하는 플러그인은 주석처리하거나 제거해둔 상태로 실행해야 결과를 더 잘 확인할 수 있습니다)

정리

웹팩은 ECMAScript2015에 등장한 모듈 시스템을 쉽게 사용할 수 있도록 해주며, 정적인 리소스들을 모듈로 제공해줄 수 있도록 해주므로 일관성 있게 개발할 수 있게 됩니다.

출처

profile
풀스택으로 나아가기

0개의 댓글