Webpack 정리 (2) — 로더와 플러그인

#Webpack#JavaScript#번들러2026-03-23

이 글은 Webpack 정리 시리즈의 두 번째 글입니다. 첫 번째 편에서는 모듈 시스템의 변천사(IIFE/CommonJS/AMD/UMD)와 그 한계를 메우는 도구로 등장한 웹팩의 기본 사용(엔트리·아웃풋)을 다뤘습니다. 이번 편에서는 웹팩을 확장하는 두 축인 로더와 플러그인을 살펴봅니다.

3. 로더 (Loader)

웹팩은 기본적으로 JavaScript 파일만 처리할 수 있습니다. 하지만 실제 애플리케이션에서는 CSS, 이미지, 폰트 등 다양한 자원을 함께 사용합니다.

웹팩이 이러한 파일들을 하나의 애플리케이션으로 묶기 위해서는, JavaScript가 아닌 자원들을 웹팩이 이해할 수 있는 형태로 변환하는 과정이 필요합니다.

이때 이 역할을 담당하는 것이 로더(Loader) 입니다.

3-1. 로더의 동작 방식

웹팩은 엔트리파일에서 시작해 import/require 구문을 따라가며 애플리케이션에 필요한 모든 모듈을 수집합니다.

이 과정에서 특정 파일을 모듈로 로드하는 순간, 해당 파일의 확장자나 조건에 맞는 로더가 적용됩니다.

즉, 로더는 번들링이 모두 끝난 뒤에 동작하는 것이 아니라, 번들링 과정 중 파일을 읽는 시점에 개입해 파일의 내용을 변환합니다.

로더의 책임은 명확합니다.

  • 파일의 원본 내용을 입력으로 받아
  • 웹팩이 처리할 수 있는 형태로 변환한 뒤
  • 그 결과를 웹팩에 다시 전달하는 것

이 역할은 실제로 하나의 함수 형태로 구현되어 있습니다. 웹팩은 파일을 로드할 때 해당 파일의 내용을 문자열로 읽어 로더에 전달하고, 로더는 이 내용을 가공한 결과를 반환합니다.

즉, 로더는 파일 내용을 입력받아 변환된 결과를 반환하는 함수로 동작합니다.

이 때문에 여러 로더를 함께 사용할 경우에도 각 로더는 이전 로더의 반환값을 입력으로 받아 순차적으로 실행될 수 있으며, 이를 통해 로더 체인(loader chain) 구조가 만들어집니다.

3-2. 커스텀 로더

로더가 함수로 동작한다는 점을 이해했으니, 단순한 커스텀 로더를 직접 만들어보며 로더의 실제 동작 방식을 살펴보겠습니다.

로더는 Node.js 환경에서 실행되기 때문에, 일반적인 Node 모듈처럼 작성할 수 있습니다. 현재 로더는 아무런 변환도 하지 않지만, 로더가 동작하는지 확인하는 용도로 로그만 출력하고 곧장 source를 리턴합니다.

// loaders/my-loader.js
module.exports = function myloader(source) {
  console.log("myloader가 동작함")
  return source
}

작성한 커스텀 로더를 사용하려면, 다른 로더와 동일하게 webpack.config.js의 module.rules를 통해 등록합니다.

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

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

    test에는 로더에 적용할 파일을 결정하는 조건입니다. 정규식을 사용해 파일 확장자나 경로 패턴을 지정할 수 있으며, 위 예제에서는 .js 확장자를 가진 모든 파일에 로더가 적용됩니다.

  • use

    use에는 해당 파일에 적용할 로더를 지정합니다. 문자열 또는 경로 형태로 로더를 지정할 수 있습니다. 문자열 또는 경로 형태로 로더를 지정할 수 있으며, 여기서는 직접 작성한 my-loader.js 파일의 경로를 전달했습니다.

3-3. 자주 사용하는 로더

이번에는 커스텀 로더가 아닌, 자주 사용하는 공식 로더들을 살펴보겠습니다. 또한 webpack 4에서 사용하던 방식과, webpack 5에서 달라진 점을 함께 정리하겠습니다. (참고한 원문이 webpack 4 버전을 다루고 있어 함께 비교)

css-loader

CSS 파일은 JavaScript가 아니기 때문에 웹팩은 이를 그대로는 처리할 수 없습니다. 이때 css-loader는 CSS파일을 JavaScript 모듈로 변환하는 역할을 합니다.

import "./style.css"

css-loader는 CSS파일을 읽어 파싱하여, JavaScript에서 import 가능한 형태로 변환하며 그 결과를 하나의 모듈로 export 합니다.

이 결과물은 정상적인 JavaScript 모듈이지만, 이 단계에서는 아직 브라우저에 스타일이 적용되지 않습니다.

브라우저가 스타일을 적용하기 위해서는 CSS를 파싱해 **CSSOM(CSS Object Model)**을 생성해야하는데, css-loader의 결과물은 CSS가 아닌 JavaScript 코드이기 때문입니다.

즉, css-loader는 CSS를 JS로 변환할 뿐, 브라우저가 CSSOM을 생성하는 단계에는 관여하지 않습니다.

style-loader

이 지점에서 style-loader가 필요해집니다. style-loader는 css-loader가 변환한 CSS 문자열을 런타임에 <style> 태그로 만들어 DOM에 삽입합니다.

const style = document.createElement("style")
style.textContent = css
document.head.appendChild(style)

이때 브라우저는 <style> 태그 안의 CSS를 파싱하고 내부적으로 CSSOM을 생성하게 됩니다.

정적 자산 처리

webpack 4에서는 이미지, 폰트와 같은 정적 자산을 처리하기 위해 주로 file-loader와 url-loader를 사용했습니다.

  • file-loader: 파일을 출력 디렉토리로 복사하고, 해당 파일의 URL을 export
  • url-loader: 파일 크기가 작을 경우 Base64문자열로 인라인 처리, 크면 내부적으로 file-loader처럼 동작

이러한 방식은 유연했지만, 정적 자산 처리를 위해 여러 로더를 설치해야하는 단점이 있었습니다.

webpack 5에서는 이러한 문제를 해결하기 위해 Asset Modules를 도입했습니다.

{
  test: /\.(png|jpg|gif)$/,
  type: "asset",
}

Asset Modules는 다음과 같은 타입을 제공합니다.

  • asset/resource → 파일 출력 (file-loader 역할)
  • asset/inline → Base64 인라인 처리 (url-loader 역할)
  • asset/source → 소스 문자열 제공 (raw-loader 역할)
  • asset → 크기에 따라 자동 선택

이로 인해 webpack 5에서는 별도의 로더 설치 없이도 대부분의 정적 자산을 처리할 수 있게 되었습니다.

4. 플러그인(Plugin)

앞에서 살펴본 로더는 파일 단위로 동작하며, 파일의 내용을 변환하는 역할을 담당합니다. 하지만 실제 빌드 과정에서는 단순한 파일 변환을 넘어 다음과 같은 작업들이 필요합니다.

  • 번들 파일 상단에 배너를 추가하거나
  • 빌드 전에 기존 산출물을 정리(clean)하거나
  • 환경 변수에 따라 다른 동작을 하거나
  • CSS를 별도의 파일로 추출하거나
  • 번들 구조(청크 분리), 최적화, 매니페스트 생성 등 “빌드 전체”를 제어

이러한 작업은 특정 파일 하나의 변환만으로는 처리할 수 없고, 번들링 과정 전체에 개입해야 합니다.

이때 사용하는 것이 플러그인(Plugin)입니다.

4-1. 플러그인의 역할

플러그인은 웹팩의 빌드 파이프라인에 개입하여 컴파일 과정 전반을 확장/제어하는 매커니즘입니다.

로더가 “파일 단위 변환기”라면, 플러그인 번들링 과정 전반을 제어하는 도구라고 볼 수 있습니다.

구분LoaderPlugin
적용 대상개별 파일(모듈)빌드 전체
역할파일 내용 변환빌드 흐름 제어
실행 시점파일 로딩 시점빌드 단계 전반
구현 형태함수클래스

4-2. 플러그인의 동작 방식

플러그인은 웹팩이 내부적으로 제공하는 이벤트(훅, hook)에 등록되어 동작합니다. 웹팩은 빌드 과정 중 여러 지점에서 훅을 실행하며, 플러그인은 이 훅에 콜백을 등록해 원하는 작업을 수행합니다.

여기서 말하는 이벤트는 브라우저의 click, load 같은 UI 이벤트가 아닙니다. 웹팩에서의 이벤트, 즉 훅(hook)은 다음을 의미합니다.

웹팩이 빌드 과정을 단계별로 쪼개 놓고, 각 단계가 시작되거나 끝나는 지점에 외부 코드가 끼어들 수 있도록 열어둔 실행 지점

웹팩 빌드는 하나의 단계적인 흐름입니다. 웹팩의 빌드는 한번에 일어나지 않고 대략적으로 다음과 같은 흐름을 가집니다.

  1. 설정 파일 로딩
  2. 컴파일 시작
  3. 엔트리 분석
  4. 모듈 그래프 생성
  5. 청크 생성
  6. 번들 파일 생성
  7. 산출물(asset) 확정
  8. 빌드 종료

웹팩은 이 각 단계마다 “지금 이 단계에 들어왔다”는 신호를 외부로 노출하는데, 이 신호가 바로 훅(hook)입니다.

4-3. 커스텀 플러그인

플러그인은 보통 클래스로 만들고, 반드시 apply() 메서드를 제공합니다.

웹팩은 플러그인을 초기화할 때 apply(compiler) 를 호출하며, 플러그인은 compiler(또는 compilation)이 제공하는 훅(hooks)에 콜백을 등록해서 동작합니다.

class MyPlugin {
  apply(compiler) {
    // compiler 훅에 접근
  }
}

module.exports = MyPlugin

빌드 타임을 출력하는 플러그인

빌드의 큰 흐름을 관찰하기 위한 플러그인을 예시로 만들어 보겠습니다.

// plugins/my-plugin.js
class MyPlugin {
  apply(compiler) {
    // 1) 웹팩이 컴파일을 시작할 때
    compiler.hooks.compile.tap("My plugin", () => {
      console.log("[MyPlugin] compile")
    })

    // 2) compilation(단일 빌드 단위)이 생성될 때
    compiler.hooks.thisCompilation.tap("MyPlugin", compilation => {
      console.log("[MyPlugin] thisCompilation")

      compilation.hooks.processAssets.tap(
        {
          name: "MyPlugin",
          stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        () => {
          console.log("[MyPlugin] processAssets")
        },
      )
    })

    // 3) 빌드가 끝나고 산출물이 확정됐을 때
    compiler.hooks.done.tap("MyPlugin", stats => {
      console.log("[MyPlugin] done")
      console.log("Output path:", compiler.options.output.path)
      console.log("Build time(ms):", stats.endTime - stats.startTime)
    })
  }
}

module.exports = MyPlugin

위 코드에서는 웹팩 플러그인의 두 가지 핵심 훅인 compiler와 compilation을 각각 사용하고 있습니다.

이때 compile, done 훅은 compiler 객체에 등록되어 있고, processAssets 훅은 compilation 객체에 등록되어 있습니다.

이는 웹팩이 빌드 과정을 두 개의 계층으로 나누어 관리하기 때문입니다.

  1. compiler: 웹팩 실행 전체를 대표하는 객체

    compiler는 웹팩이 실행될 때 단 한 번 생성되는 객체입니다.

    • 웹팩 설정을 해석하고
    • 플러그인을 등록하고
    • 빌드의 시작과 종료를 관리합니다.

    즉, compiler는 이 웹팩 실행에서 빌드가 어떻게 진행되는지를 알고 있는 최상위 컨텍스트입니다.

    compiler.hooks.compile.tap(...)
    compiler.hooks.done.tap(...)
    

    compiler의 훅들은 산출물의 내용과는 무관하며, 다음과 같이 빌드의 흐름 자체를 관찰하는 데 사용됩니다.

    • 언제 빌드가 시작됐는지
    • 언제 빌드가 끝났는지
    • 전체 빌드에 걸린 시간
  2. compilation: 한 번의 빌드 결과를 담당하는 객체

    compilation은 “산출물을 만들어가는 전 과정 전체(빌드)”를 나타내는 객체로, 다음을 모두 포함합니다.

    • 모듈 분석
    • 의존성 그래프 생성
    • 청크(chunk) 구성
    • 에셋(asset) 생성
    • 에셋 최적화 및 정리

    즉, compilation은 결과물이 만들어지기 전부터, 확장되기 직전까지의 모든 상태를 포함합니다.

    이 과정은 하나의 빌드 동안 순차적으로 진행되며, 각 단계마다 서로 다른 compilation.hooks가 실행됩니다. 이때 웹팩이 실제로 코드를 분석하고, 번들 파일과 에셋을 만들어낼 때마다 새로운 compilation 객체가 생성됩니다.

    compilation 훅에서 산출물(asset)이 이미 존재하는 것은 아닙니다. 아래의 훅들은 빌드의 분석, 준비 단계에 해당합니다.

    • buildModule, finishModules, optimizeModules

    이 시점에는 모듈과 의존성만 존재하고, 번들 파일이나 에셋은 아직 생성되지 않았습니다. 즉, 해당 단계에서는 산출물에 접근하거나 수정할 수 없습니다.

    커스텀 플러그인에 작성한 compilation 훅에서 사용한 processAssets훅은 번들 파일(asset)이 이미 생성된 이후에 실행됩니다.

    compiler.hooks.thisCompilation.tap(compilation => {
      compilation.hooks.processAssets.tap(...)
    })
    

    이 시점에는 compilation.assets 접근이 가능하고, 번들 파일의 내용을 읽고 수정할 수 있으며, 새로운 에셋을 추가하거나 기존 에셋을 교체할 수 있습니다.

    하지만 아직 실제 파일로 저장되기 전(파일 시스템에 기록되기 전)이므로 산출물을 안전하게 수정할 수 있는 마지막 단계이기도 합니다.

그러면 이제 만든 커스텀 플러그인을 사용해보기 위해 webpack.config.js에 커스텀 플러그인을 등록해보겠습니다.

// webpack.config.js
const path = require("path")
const MyPlugin = require("./plugins/my-plugin")

module.exports = {
  mode: "development",
  entry: {
    main: "./src/app.js",
  },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    clean: true, // webpack 5 내장 clean
  },
  plugins: [new MyPlugin()],
}

이후 빌드를 실행하면 콘솔에 다음과 같은 로그 흐름이 출력됩니다.

7% setup compile MyPlugin[MyPlugin] compile
8% setup compilation MyPlugin[MyPlugin] thisCompilation
92% sealing asset processing MyPlugin[MyPlugin] processAssets
99% done plugins MyPlugin[MyPlugin] done
Output path: /Users/minjihan/lecture-frontend-dev-env/dist
Build time(ms): 222

참고로 우리가 출력하는 로그 이전에 퍼센테이지와 웹팩 내부 빌드 단계 이름이 함께 표시되는 이유는, 웹팩이 빌드 진행 상황을 기본적으로 출력하고 있기 때문입니다.

플러그인의 로그는 웹팩의 진행 로그가 출력되는 동일한 시점에 실행되며, 이로 인해 하나의 로그처럼 함께 보일 수 있습니다.

4-4. 배너 출력 플러그인 (webpack 4, webpack 5 비교)

플러그인의 동작 방식 변화를 이해하기 위해 번들 파일 상단에 배너 주석을 삽입하는 커스텀 플러그인을 예제로 살펴보겠습니다. (참고한 글의 원문이 webpack 4를 기준으로 작성되어, 현재 기준으로 비교해보고자 작성하였습니다.)

webpack 4 방식의 배너 플러그인

webpack 4에서는 asset을 처리하기 위해 주로 emit 훅을 사용했습니다.

// webpack 4
class BannerPlugin {
  apply(compiler) {
    compiler.plugin("emit", (compilation, callback) => {
      const source = compilation.assets["main.js"].source()

      compilation.assets["main.js"].source = () => {
        const banner = [
          "/**",
          " * This file is bundled by webpack",
          " */",
          "",
        ].join("\n")

        return banner + "\n" + source
      }

      callback()
    })
  }
}

module.exports = BannerPlugin

위 플러그인은 다음 순서로 동작합니다.

  1. 웹팩이 모든 번들링을 끝냅니다.

  2. emit 훅이 실행됩니다.

    → 산출물이(asset)이 이미 만들어진 상태

  3. compilation.assets[’main.js’]에 접근

  4. 기존 소스 문자열 앞에 배너 문자열을 추가

  5. 수정된 결과를 다시 asset으로 설정

  6. callback() 호출로 작업 종료를 알림

즉, 이 플러그인은 파일을 파일 시스템에 입력하기 직전에 한 번 가로채서 문자열을 바꾸는 방식입니다.

위 동작 흐름에서 핵심이 되는 지점이 바로 emit 훅입니다.

emit 훅의 역할은, 번들 결과물(asset)이 이미 만들어진 뒤, 그 결과가 실제 파일로 출력 디렉토리에 기록되기 직전에 실행되는 훅입니다.

이 시점에는 다음이 가능합니다.

  • compilation.assets 접근
  • 번들 파일 내용 읽기 및 수정
  • 새로운 asset 추가 및 기존 asset 교체

따라서 webpack 4에서는 배너 삽입, 파일 헤더 추가, 간단한 후처리 작업이 대부분 emit 훅에서 이루어졌습니다.

이때 emit 훅은 비동기 훅이기 때문에, webpack은 플러그인이 언제 작업을 끝냈는지 알 수 있어야 합니다. 따라서 callback() 함수를 호출하여 해당 플러그인의 emit 단계 작업이 끝났다는 신호였습니다.

webpack 4 방식은 단순했지만 구조적인 한계를 가지고 있었습니다.

  • 모든 asset 후처리가 emit 하나에 몰림
  • 플러그인 간 실행 순서를 세밀하게 제어하기 어려움
  • asset을 직접 변조하여 webpack이 변경을 추적하기 어려움
  • callback 기반 비동기 모델로 실수 여지 존재

이러한 문제로 인해 webpack 5에서는 asset 처리 구조가 재설계됩니다.

webpack 5 방식의 배너 플러그인

webpack 5 에서는 webpack 4에서 사용되던 emit 중심의 자산 처리 구조를 개선하여, asset을 다루는 시점과 목적을 더 명확하게 분리했습니다.

이는 단순히 새로운 훅을 추가한 것이 아니라**, 언제 어떤 종류의 asset 작업이 일어나는지를 webpack이 명시적으로 관리하기 위한 구조적 변화**입니다.

// Webpack 5
class BannerPlugin {
  constructor(text) {
    this.text = text
  }

  apply(compiler) {
    const { Compilation, sources } = compiler.webpack

    compiler.hooks.thisCompilation.tap("BannerPlugin", compilation => {
      compilation.hooks.processAssets.tap(
        {
          name: "BannerPlugin",
          stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
        },
        asset => {
          const assetName = "main.js"
          if (!assets[assetName]) return

          const original = assets[assetName].source()
          const banner = ["/**", ` * ${this.text}`, " */", ""].join("\n")

          compilation.updateAsset(
            assetName,
            new sources.RawSource(banner + original),
          )
        },
      )
    })
  }
}

module.exports = BannerPlugin

위 플러그인은 다음과 같은 순서로 동작합니다.

  1. webpack이 엔트리부터 시작해 모든 모듈을 분석하고 번들링을 수행한다.
  2. 번들 결과물(asset)이 메모리에 생성된다.
  3. asset이 파일로 기록되기 전에 processAssets 훅이 실행된다.
  4. main.js asset의 기존 소스 코드를 읽는다.
  5. 배너 문자열을 기존 코드 앞에 추가한다.
  6. 수정된 내용을 새로운 asset으로 교체한다.
  7. 이후 webpack이 최종 결과를 파일 시스템이 기록한다.

1) processAssets 훅의 사용

webpack 4의 emit과 동일하게 파일로 쓰기 직전의 asset을 수정한다는 목적은 같지만, 그 작업을 수행하는 구조가 달라졌습니다.

wepack 5 에서는 자산을 다루는 공식적인 진입점으로 processAssets 훅을 제공합니다. 이 훅은 번들 결과물(asset)이 이미 생성된 이후, 아직 파일 시스템에 기록되기 전 단계의 시점을 의미합니다.

이 시점에서는

  • asset의 내용을 안전하게 수정할 수 있고,
  • 이후 최적화, 리포트 단계와 충돌하지 않으며
  • webpack이 변경 사항을 정확히 추적할 수 있는 단계입니다.

그래서 webpack 5에서는 배너 삽입과 같은 자산 후처리 작업을 emit 대신 processAssets에서 수행하도록 권장합니다.

2) stage의 명시

webpack 5의 가장 큰 변화 중 하나는, asset 처리 단계(stage)를 명시적으로 구분한다는 점입니다.

stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS

이 설정은 webpack에 다음과 같은 의미를 전달합니다.

이 플러그인은 asset에 내용을 추가(addition)하는 단계에서 실행된다

Webpack은 자산 처리 작업을 여러 단계로 나눕니다.

  • ADDITIONS: 자산 추가, 헤더/배너 삽입
  • OPTIMIZE: 압축, 정리
  • SUMMARIZE: 메타 정보 정리
  • REPORT: 통계 및 리포트 출력

webpack 4에서는 이 모든 작업이 emit 하나에 몰려 있었지만, webpack 5에서는 작업의 성격에 따라 실행 시점을 분리합니다.

3) asset 수정 방식의 차이

또한 webpack 4에서는 asset 객체를 직접 수정했습니다.

compilation.assets[name].source = () => ...

하지만 webpack 5 에서는 이를 대신해 webpack이 변경을 추적할 수 있는 API를 제공합니다.

compilation.updateAsset(assetName, new sources.RawSource(newContent))

이 방식의 장점은 다음과 같습니다.

  • 자산 변경 시점이 명확해집니다.
  • webpack 내부 최적화, 캐싱 로직과 충돌 감소
  • 플러그인 간 asset 수정 충돌 방지

4) 비동기/동기 훅이 명시적으로 드러나는 구조

마지막으로 webpack 5 에서는 훅 등록 방식 자체가 달라졌습니다.

// 동기 훅
compilation.hooks.processAssets.tap("MyPlugin", () => {})

// Promise 기반 비동기 훅
compilation.hooks.processAssets.tapPromise("MyPlugin", async () => {})

webpack 4의 emit 훅처럼 무조건 callback을 호출해야하는 구조가 아니라, 코드만 봐도 동기/비동기 여부가 명확히 드러납니다.

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

실제 프로젝트에서는 다음과 같은 플러그인들이 가장 자주 사용됩니다.

1. HtmlWebpackPlugin

번들 결과물을 자동으로 HTML 파일에 주입해주는 플러그인입니다.

번들 파일 이름에 해시가 포함되거나, 엔트리가 여러 개로 나뉘는 경우 <script src=’…’>를 사람이 직접 관리하는 것은 매우 번거롭습니다.

HtmlWebpackPlugin은 이 문제를 해결합니다.

const HtmlWebpackPlugin = require("html-webpack-plugin")

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

HtmlWebpackPlugin은 아래 순서로 동작합니다.

  • 빌드 결과로 생성된 JS/CSS asset을 분석
  • HTML 파일에 <script>, <link> 태그 자동 삽입
  • 해시가 바뀌어도 HTML 수정 불필요

2. DefinePlugin

빌드 타임에 사용할 상수를 코드에 주입하는 플러그인입니다. 흔히 환경 변수를 주입한다고 설명되지만, 정확히는 환경 변수를 전달하는 기능이 아닙니다.

DefinePlugin은 빌드 시점에 소스 코드 내 식별자를 특정 값으로 치환하는 플러그인입니다.

즉, 실행 중에 값을 읽는 것이 아니라, 번들링 과정에서 소스 코드 자체를 변경합니다.

const { DefinePlugin } = require("webpack")

module.exports = {
  plugins: [
    new DefinePlugin({
      __BETA__: JSON.stringify(true),
      "process.env.NODE_ENV": JSON.stringify("development"),
      "process.env.API_URL": JSON.stringify("https://api.example.com"),
    }),
  ],
}

DefinePlugin을 위와 같이 설정하면, webpack은 번들링 과정에서 process.env.NODE_ENV라는 식별자를 문자열 리터럴로 직접 치환합니다.

if (process.env.NODE_ENV === "production") {
  enableOptimize()
} else {
  enableDebug()
}

만약 위와 같은 코드가 있다면, 개념적으로는 아래와 같은 코드가 만들어지게 됩니다.

if ("development" === "production") {
  enableOptimize()
} else {
  enableDebug()
}

이 과정은 빌드 타임에 발생하며, 런타임에는 process.env 자체가 존재하지 않아도 됩니다.

빌드 시점에 조건식이 위와 같이 고정되면, webpack과 압축기(Terser)는 실행되지 않는 분기 코드를 제거하게 되고, 이는 트리 셰이킹과 dead code 제거에 중요한 역할을 합니다.

그 결과, 불필요한 코드가 번들에서 제거되고 최종 결과물이 더 작아집니다.

이러한 최적화는 값이 빌드 타임에 확정되어 있을 때만 가능합니다. 따라서 해당 플러그인은 보안의 목적이 아닌, 성능과 최적화를 위한 도구로, 클라이언트에 노출되어도 문제가 없는 값만 사용합니다.

3. MiniCssExtractPlugin

CSS를 JavaScript 번들에서 분리하여, 별도의 CSS 파일로 추출하는 플러그인 입니다.

webpack은 기본적으로 모든 리소스를 JavaScript 모듈 그래프 기준으로 처리하기 때문에, CSS 역시 아무 설정이 없으면 JS 번들 안에 포함된 상태로 처리됩니다.

왜 CSS를 JS에서 분리해야할까요?

개발 단계에서는 CSS를 JS로 묶는 방식이 편리합니다.

  • 즉시 반영이 가능하고
  • HMR과 잘 동작하며
  • 설정이 단순하기 때문입니다.

하지만 프로덕션 환경에서는 문제가 될 수 있습니다.

  • JS가 실행되어야 CSS가 적용되므로
  • 초기 렌더링시 스타일 적용이 늦어지고,
  • CSS 캐싱이 불리합니다.

즉, CSS는 JavaScript 실행과 무관하게, 가능한 빨리 브라우저에 로드되는 것이 유리하고, 이 문제를 해결하기 위해 사용하는 플러그인이 MiniCssExtractPlugin 입니다.

const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css",
    }),
  ],
}

이 설정은 다음과 같이 동작합니다.

  1. css-loader가 CSS 파일을 JavaScript가 이해할 수 있는 모듈 형태로 변환하고
  2. MiniCssExtractPlugin.loader가 해당 CSS를 JS 번들에서 분리하며
  3. 플러그인이 최종적으로 별도의 .css 파일을 생성합니다.

결과적으로 CSS는 <link rel="stylesheet"> 형태로 로드됩니다.

style-loader는 어디로 갔을까?

앞서 말했듯이, 개발 단계에서는 CSS를 JS에 포함시키는 방식이 유리합니다. 이때 사용하는 로더가 바로 style-loader였습니다.

style-loader와 동시 사용은 피하기

use: ["style-loader", MiniCssExtractPlugin.loader, "css-loader"]

위 설정은 잘못된 설정입니다.

  • style-loader는 CSS를 JS 안에 주입하려고 하고
  • MiniCssExtractPlugin.loader는 CSS를 JS에서 분리하려고 합니다.

즉, CSS의 목적지가 서로 충돌하기 때문에 두 로더는 대체 관계에 있습니다.

따라서, 실무에서는 보통 개발 / 프로덕션 환경에 따라 아래와 같이 분기합니다.

const isProd = process.env.NODE_ENV === "production"

{
  test: /\.css$/,
  use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader"],
}
  • 개발 환경: 빠른 반영과 HMR을 위해 style-loader
  • 프로덕션 환경: 성능과 캐싱을 위해 MiniCssExtractPlugin

정리하자면, style-loader는 개발 편의성을, MiniCssExtractPlugin은 프로덕션 최적화를 위한 도구입니다.

4. BannerPlugin

번들 파일 상단에 주석 배너를 삽입하는 플러그인입니다.

라이선스 정보, 빌드 정보 등을 삽입할 때 사용합니다.

const { BannerPlugin } = require("webpack")

module.exports = {
  plugins: [
    new BannerPlugin({
      banner: "This file is bundled by webpack",
    }),
  ],
}
  • 내부적으로 webpack 5의 processAssets 훅을 사용합니다.
  • 단순 배너 삽입에는 커스텀 플러그인보다 권장됩니다.

웹팩의 공식 지원하는 BannerPlugin은 모든 번들 파일에 항상 동일한 문자열을 무조건 삽입합니다. 따라서 조건부/동적 처리가 필요한 경우는 커스텀 배너 플러그인을 사용합니다.

5. CopyWebpackPlugin

번들링과 무관한 파일을 output 디렉토리로 그대로 복사하는 플러그인입니다.

예를 들어 favicon, robots.txt, 정적 이미지 등이 이에 해당합니다.

이는 로더로 처리할 필요 없는 (해석이 별도로 필요없는) 파일들을 의미하며, 주로 JS 코드에서 import하지 않고, 빌드 결과물에 그대로 존재하기만 하면 되는, 내용 변경이나 변환이 필요 없는 asset을 의미합니다.

const CopyWebpackPlugin = require("copy-webpack-plugin")

module.exports = {
  plugins: [
    new CopyWebpackPlugin({
      patterns: [{ from: "public", to: "public" }],
    }),
  ],
}

6. CleanWebpackPlugin (webpack 5에서 기본기능)

빌드 전에 output 디렉토리를 정리(clean)하는 플러그인입니다.

webpack 4에서는 별도 플러그인이 필요했지만, webpack 5에서는 기본 옵션으로 통합되었습니다.

module.exports = {
  output: {
    clean: true,
  },
}

따라서 더이상 clean-webpack-plugin을 설치하지 않아도 되며 기본 옵션으로 사용 가능합니다.