Native HTML

이 가이드에서는 experiments.html을 사용해 webpack의 네이티브 HTML 처리를 사용하는 방법과 기존 설정에서 html-loader 및 html-webpack-plugin을 제거하며 마이그레이션하는 방법을 설명합니다.

Getting Started

webpack 설정에서 네이티브 HTML 지원을 활성화하세요.

webpack.config.js

export default {
  experiments: {
    html: true,
  },
};

이 옵션을 활성화하면 webpack은 .html 파일을 일급 모듈로 이해하고 loader에 전달하는 대신 직접 파싱합니다.

What's built-in

"Built-in"은 체인에 loader나 plugin 없이 webpack이 직접 작업한다는 의미이며, html-loader와 html-webpack-plugin의 모든 기능을 지원한다는 의미는 아닙니다. 정확한 지원 범위는 다음과 같습니다.

내장 지원여전히 loader 또는 plugin 필요
.html을 entry point로 사용하거나 JS에서 가져올 때 파싱동기 template hook으로 충분하지 않은 경우 Pug, EJS, Handlebars 등의 template engine
페이지가 참조하는 모든 URL 추출 및 빌드된 파일 이름으로 재작성html-loader의 sources.scriptingEnabled와 postprocessor
인라인 <script> 및 <style> 본문과 style="" 속성 번들링html-webpack-plugin의 chunksSortMode, xhtml, showErrors, cache
청크가 주입된 페이지를 엔트리 포인트별로 생성아직 HtmlModulesPlugin에 대응 hook이 없는 html-webpack-plugin hook을 사용하는 서드파티 plugin
title, meta, base, favicon, manifest, integrity, csp, 인라인 처리-
Minification 및 Hot Module Replacement-

따라서 html-webpack-plugin으로 번들을 감싸는 문서를 만들거나 html-loader로 partial을 가져오는 프로젝트에는 둘 다 필요하지 않습니다. template engine으로 페이지를 렌더링하거나 html-webpack-plugin hook을 사용하는 plugin에 의존하는 프로젝트는 해당 plugin을 유지해야 합니다.

Three ways to use HTML

Native HTML은 이전에 서로 다른 두 패키지가 필요했던 세 가지 작업을 지원합니다. 프로젝트에 맞는 방식을 선택하세요. 여러 방식을 함께 사용할 수도 있습니다.

사용 사례작성할 설정대체 대상
HTML entry point — 페이지가 빌드를 주도함entry: './src/index.html'template을 사용하는 html-webpack-plugin
Generated page — webpack이 번들을 감싸는 문서를 생성함output.html: truetemplate이 없는 html-webpack-plugin
HTML imported from JavaScript — 문자열로 사용하는 partialimport page from './page.html'html-loader

1. HTML as an entry point

entry가 .html 파일을 가리키도록 하면 webpack이 페이지를 직접 빌드합니다. 모든 <script src>, <link rel="stylesheet">, <img src>, 인라인 <style>, 인라인 <script>가 모듈 그래프의 일부가 되며, 내보낸 페이지의 모든 URL은 빌드된 hash 파일 이름으로 재작성됩니다.

src/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>My App</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <img src="./logo.png" alt="Logo" />
    <script type="module" src="./index.js"></script>
  </body>
</html>

webpack.config.js

export default {
  entry: "./src/index.html",
  experiments: {
    html: true,
    css: true,
  },
};

이는 Vite와 Parcel이 사용하는 HTML 우선 모델입니다. 페이지가 필요한 script와 style의 단일 정보 소스가 되므로 동기화해야 할 별도의 엔트리 포인트 목록이 없습니다.

2. A generated page for a JavaScript entry

JavaScript 엔트리를 유지하면서 webpack이 문서를 작성하도록 하려면 output.html을 설정하세요. Webpack은 HTML이 아닌 각 엔트리 포인트마다 HTML 파일 하나를 생성하고, dependOn을 통해 공유하는 청크를 포함하여 해당 엔트리 포인트의 초기 JS와 CSS 청크를 주입합니다.

webpack.config.js

export default {
  entry: {
    main: "./src/main.js",
  },
  output: {
    html: true,
  },
  experiments: {
    html: true,
  },
};

이는 번들을 감싸는 문서를 생성하는 html-webpack-plugin의 역할에 해당합니다. 생성된 페이지를 설정하려면 output.html을 객체로 지정하세요.

3. HTML imported from JavaScript

JavaScript에서 .html 파일을 가져오면 모든 애셋 참조가 webpack을 통해 리졸브된 처리 결과 HTML을 문자열로 얻습니다. 이는 오랫동안 html-loader가 담당했던 역할입니다.

src/index.js

import page from "./page.html";

document.querySelector("#app").innerHTML = page;

이 방식으로 가져온 HTML 모듈은 기본적으로 독립 파일로 내보내지 않지만, 엔트리로 사용한 HTML 모듈은 파일로 내보냅니다. module.generator.html.extract를 사용해 두 동작을 재정의할 수 있습니다.

What webpack bundles from a page

기본적으로 parser는 알려진 URL 포함 속성을 의존성으로 처리합니다. 다음 항목은 별도 설정 없이 처리됩니다.

마크업동작
<script src>classic 청크 엔트리가 되며 src는 내보낸 청크를 가리키도록 재작성됩니다.
<script type="module" src>ES module 청크 엔트리가 됩니다.
<script>…</script> (inline)본문이 자체 엔트리가 되고 태그는 <script src>로 재작성됩니다.
<style>…</style> (inline)CSS 파이프라인을 거치며 @import/url()을 리졸브한 텍스트를 다시 기록합니다.
style="…" 속성declaration 목록으로 CSS 파이프라인을 거칩니다.
<link rel="stylesheet">CSS 청크 엔트리가 됩니다.
<link rel="modulepreload">독립 엔트리가 됩니다(미리 로드되며 sibling에서 실행되지 않음).
<link rel="preload"> / <link rel="prefetch">script/style을 번들링하고 href를 재작성합니다.
<link rel="icon">, <link rel="manifest">hash 애셋으로 내보내며 manifest의 icons/screenshots/shortcuts도 처리합니다.
<img src>, <img srcset>, <source srcset>hash 애셋으로 내보냅니다.
<iframe srcdoc>내부 문서를 HTML 파이프라인으로 번들링합니다.
SVG 참조(fill, cursor, font-face-uri, …)url(...) 대상을 애셋으로 내보냅니다.
<meta name="twitter:player:stream">hash 애셋으로 내보냅니다.

다음 두 규칙으로 JavaScript 측의 동작을 예측할 수 있습니다.

  • 같은 페이지의 여러 <script src> 태그는 하나의 runtime을 공유합니다. 각 그룹(classic 또는 type="module")에서 첫 번째 항목이 runtime을 보유하고 나머지는 이에 대한 dependOn을 선언합니다.
  • output.module을 활성화하면 classic <script> 태그가 자동으로 type="module"로 변경되어 내보낸 ES module 청크가 올바른 모드로 로드됩니다.

JS가 아닌 script 타입(application/ld+json, importmap 등), data URI, CSS가 아닌 type을 가진 <style>은 변경하지 않고 그대로 전달합니다.

Skipping a single URL

태그 바로 앞에 webpackIgnore 주석을 추가하면 URL을 처리하지 않습니다. CDN 애셋이나 서버가 런타임에 재작성하는 URL에 유용합니다.

<!-- webpackIgnore: true -->
<script src="https://cdn.example.com/analytics.js"></script>

Customizing which attributes are URLs

module.parser.html.sources는 전체 목록을 제어합니다. literal "..."을 사용하면 내장 기본값을 유지하면서 사용자 정의 항목을 추가할 수 있습니다.

webpack.config.js

export default {
  experiments: { html: true },
  module: {
    parser: {
      html: {
        sources: [
          "...", // 내장 기본값 유지
          { tag: "img", attribute: "data-src", type: "src" },
          { tag: "img", attribute: "data-srcset", type: "srcset" },
          { attribute: "data-href", type: "src" }, // 모든 태그
          { tag: "img", attribute: "src", type: false }, // 내장 기본값 제거
        ],
      },
    },
  },
};

URL 추출을 완전히 비활성화하려면 sources: false를 전달하세요. 인라인 <script>와 <style> 본문은 계속 처리됩니다. filter callback을 사용하면 개별 요소를 건너뛸 수 있습니다.

export default {
  experiments: { html: true },
  module: {
    parser: {
      html: {
        sources: [
          "...",
          {
            tag: "img",
            attribute: "src",
            type: "src",
            // CDN의 절대 URL은 변경하지 않습니다.
            filter: (attributes, value) => !value.startsWith("https://"),
          },
        ],
      },
    },
  },
};

Linking pages together

html source 타입은 다른 페이지를 가리키는 href를 빌드에 포함합니다. 연결된 파일은 자체 페이지로 번들링되어 출력되고, 속성은 해당 출력 파일 이름으로 재작성됩니다.

webpack.config.js

export default {
  entry: "./src/index.html",
  experiments: { html: true },
  module: {
    parser: {
      html: {
        sources: ["...", { tag: "a", attribute: "href", type: "html" }],
      },
    },
  },
};
<!-- src/index.html -->
<a href="./about.html">About</a>

Configuring the generated page

아래의 모든 옵션은 output.html 아래에 있으며 webpack이 생성한 페이지에 적용됩니다. entry descriptor를 통해 엔트리별로 재정의할 수 있습니다.

webpack.config.js

export default {
  entry: {
    app: "./src/app.js",
    // 이 엔트리에는 HTML 페이지를 생성하지 않습니다.
    worker: { import: "./src/worker.js", html: false },
    // 이 엔트리는 옵션 하나만 재정의합니다.
    admin: { import: "./src/admin.js", html: { title: "Admin" } },
  },
  output: { html: { title: "My App" } },
  experiments: { html: true },
};

Title, meta and base

webpack.config.js

export default {
  experiments: { html: true },
  output: {
    html: {
      title: "My App",
      meta: {
        viewport: "width=device-width, initial-scale=1",
        description: "A webpack-built app",
        "og:title": "My App",
      },
      base: { href: "/app/", target: "_self" },
    },
  },
};

charset 키는 charset 선언으로 특별 처리되며(meta: { charset: "UTF-8" }은 <meta charset="UTF-8">을 내보냄), og:로 시작하는 키는 name 대신 property 속성을 사용합니다. 페이지에 이미 해당 선언이 있으면 각 항목을 건너뛰므로 직접 작성한 페이지의 값이 항상 우선합니다.

Where the tags go

output.html.inject는 주입된 청크 태그의 위치를 지정합니다. 'body'가 기본값이고 output.module에서는 'head'가 기본값이며, 'head'를 직접 지정하거나 false로 sibling 청크 주입을 막을 수 있습니다. output.html.scriptLoading은 로드 방식을 선택합니다. 기본값인 'auto'는 ES module 출력에는 module script를, 그 외에는 defer를 사용하며, 'defer' 또는 'blocking'을 직접 지정할 수도 있습니다.

export default {
  experiments: { html: true },
  output: {
    html: {
      inject: "head",
      scriptLoading: "defer",
    },
  },
};

페이지에 <head>가 있으면 stylesheet <link> 태그는 항상 첫 번째 blocking script 앞의 <head>에 배치됩니다.

Inlining critical chunks

output.html.inline은 청크를 연결하는 대신 내용을 페이지에 직접 기록합니다. 이는 기존 html-webpack-inline-source-plugin이 담당하던 작업입니다. 페이지의 [contenthash]에는 인라인 콘텐츠가 반영됩니다.

export default {
  experiments: { html: true },
  output: {
    html: {
      // `true`는 모두 인라인 처리하고 `'script'`/`'style'`은 타입을 제한합니다.
      inline: [/^runtime/, /critical/],
    },
  },
};

직접 작성한 페이지에서는 webpackInline magic comment로 개별 참조의 인라인 여부를 지정할 수 있습니다.

<!-- webpackInline: true -->
<script src="./critical.js"></script>

Favicons and the web app manifest

export default {
  experiments: { html: true },
  output: {
    html: {
      favicon: {
        icon: [
          { href: "./favicon.svg", type: "image/svg+xml" },
          { href: "./favicon-32.png", sizes: "32x32" },
          {
            href: "./favicon-dark.png",
            media: "(prefers-color-scheme: dark)",
          },
        ],
        "apple-touch-icon": "./apple-touch-icon.png",
      },
      manifest: {
        name: "My App",
        short_name: "App",
        start_url: "/",
        display: "standalone",
        icons: [{ src: "./icon-512.png", sizes: "512x512" }],
      },
    },
  },
};

manifest 안에 지정한 항목을 포함한 모든 icon은 일반 파이프라인을 통해 hash 애셋으로 내보냅니다. 두 옵션 모두 페이지 이름을 받는 함수도 허용하므로 멀티 페이지 빌드의 각 페이지에 서로 다른 icon 세트를 지정할 수 있습니다.

두 가지 제한 사항을 알아두세요. Webpack은 지정한 icon을 내보내지만 favicons-webpack-plugin 같은 전용 plugin처럼 하나의 원본 이미지에서 크기와 형식이 다른 variant를 생성하지는 않습니다. 또한 두 옵션은 webpack이 생성한 페이지에만 적용됩니다. 직접 작성한 페이지는 그대로 유지되므로 마크업을 직접 관리한다면 페이지에 <link> 태그를 추가해야 합니다.

Subresource Integrity and CSP

export default {
  experiments: { html: true },
  output: {
    crossOriginLoading: "anonymous",
    html: {
      integrity: true, // 또는 ['sha256', 'sha384']
      csp: {
        policy: {
          "img-src": ["'self'", "data:"],
          "connect-src": "https://api.example.com",
        },
      },
    },
  },
};

integrity: true는 주입된 <script>/<link> 태그에 sha384 integrity 속성을 추가합니다. SRI에는 CORS가 활성화된 요청이 필요하므로 output.crossOriginLoading과 함께 사용하세요. csp는 엄격한 기본 policy와 모든 인라인 <script>/<style>의 hash를 포함하는 <meta http-equiv="Content-Security-Policy">를 주입합니다. 서버가 요청마다 값을 재작성한다면 대신 nonce를 설정하세요. 두 옵션은 webpack-subresource-integrity와 csp-html-webpack-plugin을 대체합니다.

Resource hints

output.resourceHints는 페이지에 <link rel="preload">/prefetch/modulepreload/preconnect 태그를 내보냅니다. 이는 기존 preload-webpack-plugin이 담당하던 작업입니다.

export default {
  experiments: { html: true },
  output: {
    resourceHints: {
      initial: true, // 초기 의존성 그래프에 hint 추가
      preconnect: true, // 빌드가 참조하는 origin에 미리 연결
      urlHints: [{ test: /\.woff2$/, preload: true, as: "font" }],
    },
  },
};

배열을 사용하면 literal href 및 이름이 지정된 청크나 엔트리에 대한 참조까지 완전히 제어할 수 있습니다.

export default {
  experiments: { html: true },
  output: {
    resourceHints: [
      { rel: "preconnect", href: "https://cdn.example.com" },
      {
        rel: "preload",
        href: "/fonts/inter.woff2",
        as: "font",
        type: "font/woff2",
        crossorigin: true,
      },
      { rel: "prefetch", entry: "settings" },
      { rel: "preload", chunk: "runtime", fetchPriority: "high" },
    ],
  },
};

CSS에서 참조하는 폰트에는 module.parser.css.fontPreload가 hint를 자동으로 추가합니다. server-side rendering에서는 resourceHints.manifest가 리졸브된 hint 목록을 JSON 애셋에 기록하여 서버가 직접 태그를 주입할 수 있도록 합니다.

Templating

module.parser.html.template은 parser가 의존성을 추출하기 전에 원본 HTML을 변환하므로 template 언어가 생성한 URL도 탐색하여 번들링합니다. 직접 작성한 페이지와 생성된 페이지 모두에서 실행됩니다.

webpack.config.js

export default {
  experiments: { html: true },
  module: {
    parser: {
      html: {
        template: (source, { resource, addDependency }) => {
          addDependency(resource);
          return source
            .replaceAll("{{title}}", "Hello world")
            .replaceAll("{{image}}", "./image.png");
        },
      },
    },
  },
};

context 객체는 현재 module과 해당 resource, 빌드 의존성 helper(addDependency, addContextDependency, addMissingDependency, addBuildDependency), emitWarning/emitError를 제공합니다. 읽는 template 파일을 등록해야 watch mode가 변경 사항을 감지합니다.

모든 동기 template engine을 같은 방식으로 연결할 수 있습니다.

import fs from "node:fs";
import { fileURLToPath } from "node:url";
import { compile } from "handlebars";

const dataFile = fileURLToPath(new URL("./src/data.json", import.meta.url));

export default {
  experiments: { html: true },
  module: {
    parser: {
      html: {
        template: (source, { addDependency }) => {
          // 데이터가 변경되면 페이지를 다시 빌드합니다.
          addDependency(dataFile);
          const data = JSON.parse(fs.readFileSync(dataFile, "utf8"));
          return compile(source)(data);
        },
      },
    },
  },
};

Parsing a fragment

HTML partial은 문서가 아니므로 문서로 파싱하면 context에 민감한 태그가 누락됩니다. 예를 들어 table 밖의 단독 <tr>이 그렇습니다. module.parser.html.as는 소스를 파싱할 context 요소의 이름을 지정합니다.

export default {
  experiments: { html: true },
  module: {
    rules: [
      {
        test: /\.rows\.html$/i,
        type: "html",
        parser: { as: "tbody" },
      },
      {
        test: /\.partial\.html$/i,
        type: "html",
        parser: { as: "template" }, // 중립적인 fragment
      },
    ],
  },
};

Minification

optimization.minimize가 활성화되면 내장 minimizer가 HTML 애셋을 최소화합니다. mode: 'production'에서는 기본적으로 활성화되며 별도로 설치하거나 연결할 항목이 없습니다.

webpack.config.js

export default {
  mode: "production",
  experiments: { html: true },
};

문서의 DOM을 유지하는 모든 변환은 기본적으로 활성화됩니다. script나 selector가 다시 읽는 값을 변경하는 변환은 명시적으로 활성화해야 합니다. 객체 형태의 optimization.minimize로 설정하세요.

export default {
  mode: "production",
  experiments: { html: true },
  optimization: {
    minimize: {
      html: {
        collapseWhitespace: "smart",
        removeRedundantAttributes: "smart",
        // 명시적으로 활성화 — DOM이 다시 읽는 값을 변경합니다.
        sortAttributes: true,
        sortTokenLists: true,
        mergeStyles: true,
      },
    },
  },
};

인라인 <style> 요소와 style="" 속성은 optimization.minimize.css 옵션을 사용하는 CSS minimizer로 최소화되므로 스타일시트와 동일한 인라인 declaration을 같은 방식으로 처리합니다. HTML 최소화만 비활성화하려면 minimize: { html: false }를 사용하세요.

Hot Module Replacement

HTML 모듈은 별도 설정 없이 Hot Module Replacement를 지원합니다 — devServer.hot을 통해 HMR이 활성화될 때마다 함께 활성화됩니다. 실제 .html 파일로 추출된 페이지에서는 각 업데이트가 document.body.innerHTML과 document.title을 제자리에서 패치합니다. 다른 <head> 변경 사항은 델타로 제자리에서 패치되며, 이미 실행된 <script>가 제거되거나 순서가 바뀌는 경우에만 전체 리로드로 폴백합니다.

Plugin hooks

webpack.html.HtmlModulesPlugin은 injectTags, transformTags, transformHtml, htmlEmitted compilation hook을 노출합니다. 이는 html-webpack-plugin의 hook이 제공하던 확장 지점입니다. HtmlModulesPlugin.getCompilationHooks를 참고하세요.

class AddBuildStampPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap("AddBuildStamp", (compilation) => {
      const hooks =
        compiler.webpack.html.HtmlModulesPlugin.getCompilationHooks(
          compilation,
        );

      hooks.transformHtml.tap("AddBuildStamp", (html) =>
        html.replace("</body>", `<!-- built ${Date.now()} --></body>`),
      );
    });
  }
}

Migrating from html-loader

html-loader는 가져온 .html 파일의 URL을 리졸브하고 문자열로 변환했습니다. Native HTML은 loader 없이 같은 작업을 수행하므로 마이그레이션은 대부분 기존 설정을 삭제하는 과정입니다.

At a glance

html-loader 옵션네이티브 대안
sourcesmodule.parser.html.sources — 기본값은 true
sources.listsources의 배열 형태(기본값을 유지하려면 "..." 사용)
sources.urlFiltersource 항목의 filter callback 또는 webpackIgnore 주석
preprocessormodule.parser.html.template
postprocessortransformHtml compilation hook
minimizeoptimization.minimize — production에서 기본 활성화
esModule해당 없음 — HTML 모듈은 처리된 HTML을 default export로 내보냄

Before

webpack.config.js

export default {
  module: {
    rules: [
      {
        test: /\.html$/i,
        loader: "html-loader",
        options: {
          sources: {
            list: ["...", { tag: "img", attribute: "data-src", type: "src" }],
          },
          minimize: true,
        },
      },
    ],
  },
};

After

webpack.config.js

export default {
  experiments: {
    html: true,
  },
  module: {
    parser: {
      html: {
        sources: ["...", { tag: "img", attribute: "data-src", type: "src" }],
      },
    },
  },
};

기존 import 문은 변경되지 않습니다.

import page from "./page.html";

Migrating from html-webpack-plugin

html-webpack-plugin은 번들을 감싸는 문서 생성과 template 렌더링이라는 두 작업을 수행했습니다. Native HTML은 이를 분리합니다. output.html이 문서를 생성하고 HTML entry point 또는 module.parser.html.template이 렌더링합니다.

At a glance

html-webpack-plugin네이티브 대안
template 없음output.html: true
template파일을 HTML entry point로 사용
templateContent / templateParametersmodule.parser.html.template
filenameoutput.htmlFilename
titleoutput.html.title
metaoutput.html.meta
baseoutput.html.base
injectoutput.html.inject
scriptLoadingoutput.html.scriptLoading
faviconoutput.html.favicon
publicPathoutput.publicPath
minifyoptimization.minimize
hashoutput.filename의 [contenthash]
chunks / excludeChunks엔트리 포인트별 페이지 하나, entry descriptor html: false로 특정 엔트리 제외
여러 plugin instance(MPA)여러 엔트리
chunksSortMode해당 없음 — 주입 순서는 청크 그래프를 따름
cache, showErrors, xhtml해당 없음
plugin hookHtmlModulesPlugin hook

함께 사용하던 plugin도 내장 기능으로 대체됩니다.

Plugin네이티브 대안
html-webpack-inline-source-pluginoutput.html.inline
webpack-subresource-integrityoutput.html.integrity
csp-html-webpack-pluginoutput.html.csp
preload-webpack-pluginoutput.resourceHints
favicons-webpack-pluginoutput.html.favicon / manifest

Without a template

변경 전

import HtmlWebpackPlugin from "html-webpack-plugin";

export default {
  entry: { main: "./src/main.js" },
  plugins: [
    new HtmlWebpackPlugin({
      title: "My App",
      scriptLoading: "defer",
      favicon: "./src/favicon.png",
      meta: { viewport: "width=device-width, initial-scale=1" },
    }),
  ],
};

변경 후

export default {
  entry: { main: "./src/main.js" },
  experiments: { html: true },
  output: {
    html: {
      title: "My App",
      scriptLoading: "defer",
      favicon: "./src/favicon.png",
      meta: { viewport: "width=device-width, initial-scale=1" },
    },
  },
};

With a template

번들만 나열하던 template은 엔트리 자체가 됩니다. 실제로 필요한 <script>와 <link> 태그가 소스 파일을 가리키도록 작성하면 webpack이 이를 빌드된 애셋으로 재작성합니다.

변경 전

import HtmlWebpackPlugin from "html-webpack-plugin";

export default {
  entry: { main: "./src/main.js" },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html",
    }),
  ],
};
<!-- src/index.html -->
<!doctype html>
<html lang="en">
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- html-webpack-plugin이 여기에 script를 주입함 -->
  </body>
</html>

변경 후

export default {
  entry: { index: "./src/index.html" },
  experiments: { html: true, css: true },
};
<!-- src/index.html -->
<!doctype html>
<html lang="en">
  <head>
    <title>My App</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>

이제 페이지 자체가 로드 대상을 명시하므로 entry 목록과 template을 별도로 일치시킬 필요가 없습니다.

Multi-page application

페이지마다 plugin instance를 하나씩 사용하는 대신 페이지별 엔트리를 사용하세요.

변경 전

import HtmlWebpackPlugin from "html-webpack-plugin";

export default {
  entry: { home: "./src/home.js", about: "./src/about.js" },
  plugins: [
    new HtmlWebpackPlugin({ filename: "home.html", chunks: ["home"] }),
    new HtmlWebpackPlugin({ filename: "about.html", chunks: ["about"] }),
  ],
};

변경 후

export default {
  entry: {
    home: "./src/home.html",
    about: "./src/about.html",
  },
  output: {
    htmlFilename: "[name].html",
  },
  experiments: { html: true, css: true },
};

각 페이지는 자신이 참조하는 청크만 정확히 가져오므로 chunks/excludeChunks가 처리할 일이 없습니다. 생성된 페이지에서도 output.html: true와 페이지별 JavaScript 엔트리를 사용하면 같은 방식으로 동작합니다.

All options with examples

HTML은 네 곳에서 설정합니다. module 아래의 parser와 generator, 내보낸 페이지를 정의하는 output 옵션, optimization 아래의 minimizer 옵션입니다. 아래 표에는 모든 옵션과 각 reference 문서 링크가 나와 있습니다.

Parser options

module.parser.html 아래에서 전역으로 설정하거나, type이 html인 규칙의 parser를 사용해 규칙별로 설정하세요.

옵션타입기본값설명
sourcesboolean | Array<'...' | SourceEntry>trueURL로 처리할 속성 값과 각 항목의 번들링 방식을 지정합니다.
as'document' | string'document'전체 페이지 또는 지정한 요소의 내부 HTML(fragment)을 파싱합니다.-
urlHintsUrlHintRule[][]parser가 참조하는 애셋의 기본 resource hint 규칙입니다.--
template(source, context) => string—의존성을 추출하기 전에 HTML 소스를 변환합니다.--

webpack.config.js

export default {
  experiments: { html: true },
  module: {
    parser: {
      html: {
        sources: true,
        as: "document",
        urlHints: [{ test: /\.woff2$/, preload: true, as: "font" }],
        template: (source) => source.replaceAll("{{title}}", "My App"),
      },
    },
  },
};

Source entries

sources 배열의 각 항목은 문자열 "..."(내장 기본값 포함) 또는 객체입니다.

필드타입필수설명
attributestring예값이 URL인 속성입니다.
typesource types 참고 또는 false예값을 파싱하고 번들링하는 방식이며 false는 내장 항목을 제거합니다.
tagstring아니요일치시킬 태그 이름입니다. 생략하면 모든 요소와 일치합니다.
filter(attributes: Map<string, string>, value: string) => boolean아니요특정 요소에서 이 항목을 건너뛰려면 false를 반환합니다.

"..."이 없는 배열은 내장 목록을 완전히 사용하지 않습니다. 인라인 <script>와 <style> 본문은 sources 값과 관계없이 처리되며, URL 추출을 완전히 비활성화하는 값은 sources: false뿐입니다.

Source types

type값의 의미번들링 방식
srcURL 하나일반 애셋(<img src>)
srcsetsrcset 후보 목록각 URL을 일반 애셋으로 처리
css-urlurl(...)을 포함한 CSS 값각 참조를 일반 애셋으로 처리(SVG presentation 속성)
scriptURL<script src>와 같은 classic 청크 엔트리
script-moduleURL<script type="module" src>와 같은 ES module 청크 엔트리
stylesheetURL<link rel="stylesheet">와 같은 CSS 청크 엔트리
htmlURL다른 페이지를 번들링해 내보내고 속성에 해당 파일 이름 지정
stylesheet-style전체 스타일시트CSS 파이프라인을 거쳐 처리된 CSS가 속성 내용을 대체
stylesheet-style-attributedeclaration 목록(style="" 형식)block 내용으로 CSS 파이프라인을 거침
srcdocentity로 인코딩된 HTML 문서HTML 파이프라인을 거쳐 처리된 HTML이 속성 내용을 대체
false—해당 tag/attribute 조합의 내장 source를 비활성화

webpack.config.js

export default {
  experiments: { html: true, css: true },
  module: {
    parser: {
      html: {
        sources: [
          "...",
          { tag: "img", attribute: "data-src", type: "src" },
          { tag: "img", attribute: "data-srcset", type: "srcset" },
          { tag: "a", attribute: "href", type: "html" },
          { tag: "my-widget", attribute: "styles", type: "stylesheet-style" },
          {
            tag: "my-widget",
            attribute: "css",
            type: "stylesheet-style-attribute",
          },
          { tag: "template", attribute: "data-html", type: "srcdoc" },
          { tag: "circle", attribute: "fill", type: "css-url" },
          { attribute: "data-worker", type: "script-module" },
          // 기본값은 유지하되 `<img src>`를 URL로 처리하지 않습니다.
          { tag: "img", attribute: "src", type: false },
        ],
      },
    },
  },
};

Generator options

옵션타입기본값설명
extractboolean | 'inline'HTML 엔트리는 true, import한 경우 false처리된 HTML을 독립 .html 출력 파일로 내보낼지 여부입니다.

'inline'은 자체 파일을 내보내지 않고 HTML을 처리한 뒤 이를 포함하는 문서에 다시 기록하도록 반환합니다. <iframe srcdoc>에 필요한 동작입니다.

webpack.config.js

export default {
  experiments: { html: true },
  module: {
    rules: [
      // JS에서 가져온 partial: 파일 없이 문자열만 반환합니다.
      {
        test: /\.partial\.html$/i,
        type: "html",
        generator: { extract: false },
      },
      // JS에서 가져왔지만 계속 내보내야 하는 페이지입니다.
      {
        test: /\.page\.html$/i,
        type: "html",
        generator: { extract: true },
      },
    ],
  },
};

Output options

output.html 아래의 모든 항목은 webpack이 JavaScript 엔트리 포인트용으로 생성하는 페이지를 정의합니다. 직접 작성한 페이지는 자체 마크업을 유지하며, 이 옵션 중 inject, inline, integrity, csp만 주입되는 내용에 영향을 줍니다. scriptLoading은 생성된 페이지에만 적용됩니다.

옵션타입기본값설명
titlestring—페이지의 <title>이며 이미 있으면 건너뜁니다.---
metaobject—<meta> 태그이며 charset와 og: 키는 특별 처리됩니다.---
basestring | { href, target }—<base> 요소이며 이미 있으면 건너뜁니다.--
inject'body' | 'head' | false'body' (output.module에서는 'head')주입된 청크 태그의 위치입니다.-
scriptLoading'auto' | 'defer' | 'blocking''auto'주입된 <script> 태그의 로드 방식입니다.-
inlineboolean | 'script' | 'style' | RegExp[]false일치하는 청크를 페이지에 인라인으로 삽입합니다.
faviconboolean | string | object | functionfalseicon <link>이며 각 icon을 hash 애셋으로 내보냅니다.
manifestfalse | string | object | functionfalse연결하거나 직렬화해 내보낼 web app manifest입니다.
integrityboolean | string[] | functionfalse주입된 태그의 Subresource Integrity 속성입니다.-
cspboolean | { policy, hashFunction, nonce }false<meta http-equiv="Content-Security-Policy">입니다.--
output.htmlFilenamestring | function.html을 사용한 output.filename초기 페이지의 파일 이름 template입니다.--
output.htmlChunkFilenamestring | function.html을 사용한 output.chunkFilenameon-demand 페이지의 파일 이름 template입니다.--

webpack.config.js

export default {
  entry: { main: "./src/main.js" },
  experiments: { html: true, css: true },
  output: {
    htmlFilename: "[name].html",
    htmlChunkFilename: "pages/[name].[contenthash].html",
    crossOriginLoading: "anonymous",
    html: {
      title: "My App",
      meta: { viewport: "width=device-width, initial-scale=1" },
      base: { href: "/app/", target: "_self" },
      inject: "head",
      scriptLoading: "defer",
      inline: [/^runtime/],
      favicon: { icon: "./src/favicon.svg" },
      manifest: { name: "My App", short_name: "App" },
      integrity: ["sha384"],
      csp: { policy: { "img-src": ["'self'", "data:"] } },
    },
  },
};

favicon, manifest, integrity 옵션은 함수도 받을 수 있으므로 멀티 페이지 빌드에서 페이지 또는 애셋별로 값을 다르게 지정할 수 있습니다.

export default {
  experiments: { html: true },
  output: {
    html: {
      favicon: (name) => `./src/icons/${name}.svg`,
      manifest: (name) => (name === "app" ? "./src/app.webmanifest" : false),
      // CDN이 재작성하는 청크에는 SRI를 적용하지 않습니다.
      integrity: ({ filename }) =>
        filename.startsWith("vendor/") ? false : ["sha384"],
    },
  },
};

엔트리별 재정의에는 entry descriptor의 html 옵션을 사용합니다. 동일한 값을 받으며 output.html 위에 옵션별로 병합됩니다.

export default {
  entry: {
    app: "./src/app.js",
    admin: { import: "./src/admin.js", html: { title: "Admin", csp: true } },
    worker: { import: "./src/worker.js", html: false },
  },
  output: { html: { title: "My App" } },
  experiments: { html: true },
};

Resource-hint options

output.resourceHints는 shorthand로 initial 값을 직접 받거나 전체 객체를 받습니다.

옵션타입기본값설명
initialboolean | 'preload' | 'prefetch' | 'none' | HtmlResourceHint[] | functionESM 출력은 true, 그 외에는 false엔트리의 초기 의존성 청크에 대한 hint입니다.
urlHintsUrlHintRule[][]URL 참조 애셋에 적용할 프로젝트 전체 규칙이며 모든 parser에 적용됩니다.-----
preconnectbooleanfalsecross-origin output.publicPath origin에 미리 연결합니다.-----
dedupebooleanfalse문서가 이미 hint한 청크에 런타임이 주입하는 prefetch를 건너뜁니다.-----
modulePreloadPolyfillbooleanoutput.environment.modulePreload에서 가져옴추출된 페이지에 인라인 <link rel="modulepreload"> polyfill을 주입합니다.-----
manifeststring—엔트리 포인트별로 리졸브된 hint를 이 경로의 JSON 애셋으로 내보냅니다.-----

'none'은 완전한 비활성화 값으로 어떤 <link>도 만들지 않고 stats와 manifest도 비웁니다. false는 청크 hint만 비활성화하며 urlHints와 magic comment는 계속 작동합니다.

initial 배열(그리고 배열로 사용한 output.resourceHints)의 각 descriptor는 HtmlResourceHint입니다.

필드타입설명
rel'preload' | 'prefetch' | 'modulepreload' | 'preconnect' | 'dns-prefetch'필수이며 hint의 rel입니다.
hrefstring그대로 사용하는 literal URL입니다.----
chunkstring청크 이름이며 내보낸 URL을 자동으로 리졸브합니다.----
entrystring엔트리 포인트 이름이며 초기 청크별 hint로 확장됩니다.----
asstringas 속성이며 청크/엔트리 참조의 기본값은 script입니다.----
typestringMIME 타입입니다.----
mediastringmedia 속성입니다.----
crossoriginboolean | 'anonymous' | 'use-credentials'CORS 모드이며 true는 anonymous를 의미합니다.--
fetchPriority'low' | 'high' | 'auto'fetchpriority 속성입니다.--
integritybooleanoutput.html.integrity를 따르며 false는 이 hint를 제외합니다.----

href/chunk/entry 중 정확히 하나만 대상을 지정합니다. 아무것도 리졸브하지 못한 descriptor는 경고 없이 제거됩니다.

output.resourceHints.urlHints 및 각 parser의 urlHints에서 사용하는 UrlHintRule은 요청을 기준으로 애셋을 일치시키고 magic comment와 같은 값을 설정합니다.

필드타입설명
test / include / excludeRuleSetCondition애셋 요청과 일치시킵니다. 모두 생략하면 모든 항목과 일치합니다.---
preload / prefetchboolean일치하는 애셋에 내보낼 hint를 지정합니다.---
as, type, mediastring내보낸 <link>의 속성입니다.---
fetchPriority'low' | 'high' | 'auto' | falsefetchpriority 속성입니다.

webpack.config.js

export default {
  experiments: { html: true },
  output: {
    module: true,
    resourceHints: {
      initial: true,
      preconnect: true,
      dedupe: true,
      modulePreloadPolyfill: false,
      manifest: "resource-hints.json",
      urlHints: [
        { test: /\.woff2$/, preload: true, as: "font", type: "font/woff2" },
        {
          include: /\/hero\//,
          preload: true,
          as: "image",
          media: "(min-width: 800px)",
        },
        {
          test: /\.png$/,
          exclude: /\/hero\//,
          prefetch: true,
          fetchPriority: "low",
        },
      ],
    },
  },
};

명시적인 magic comment는 항상 규칙보다 우선하고 규칙은 기본값보다 우선합니다. 각 엔트리 포인트에 대해 리졸브된 목록은 stats의 entrypoints[name].resourceHints에서 확인할 수 있으며, manifest 애셋은 이 값을 server-side renderer용으로 직렬화합니다.

Minifier options

optimization.minimize.html의 모든 옵션을 기본 활성화 여부에 따라 분류했습니다. 비활성화된 옵션은 script, selector 또는 byte 단위 비교에서 다시 읽는 값을 변경하므로 명시적으로 활성화해야 합니다.

기본 활성화

옵션타입기본값설명
collapseBooleanAttributesboolean | 'all'truedisabled="disabled"를 속성 이름만 남기도록 변경합니다. 'all'은 모든 값을 다시 작성합니다.----
commentsboolean | 'all' | 'some' | string | RegExp | function'some'유지할 주석을 지정합니다. 'some'은 HTML 주석이 동작하지 않으므로 아무것도 유지하지 않습니다.
normalizeAttributeQuotesbooleantrue비용이 가장 적은 구분자를 사용합니다.-----
normalizeEnumeratedAttributesbooleantrue열거 값을 해당 keyword로 축약합니다.-----
normalizeListAttributesbooleantrue목록 형태의 값(class, rel, srcset, viewport content)을 정규화합니다.-----
normalizeNumericAttributesbooleantrue규칙에 맞는 단일 방식으로 정수 속성을 작성합니다.-----
removeImpliedTagsboolean | 'smart' | 'all''smart'<html>/<head>/<body> shell을 암시적으로 처리할 범위입니다.---
removeOptionalTagsbooleantrueparser가 암시할 수 있는 다른 태그를 생략합니다.-----

기본 비활성화

옵션타입기본값명시적으로 활성화해야 하는 이유
collapseWhitespaceboolean | 'conservative' | 'smart' | 'all'falseminimizer가 볼 수 없는 textContent와 white-space: pre 규칙이 축소된 공백을 다시 읽습니다. true는 'conservative'입니다.
mergeStylesbooleanfalse요소가 제거되어 document.styleSheets와 style:nth-child() 결과가 달라집니다.---
minifyConditionalCommentsbooleanfalse주석 위치가 아니라 문서 시작 지점의 본문처럼 최소화합니다.---
removeEmptyAttributesbooleanfalse속성 selector는 존재 여부로 일치하므로 [class]가 더 이상 일치하지 않습니다.---
removeEmptyElementsbooleanfalseminimizer가 확인할 수 없는 CSS가 빈 요소에 크기나 ::before를 부여할 수 있습니다.---
removeRedundantAttributesboolean | 'smart' | 'all'false속성을 제거하면 getAttribute와 속성 selector의 결과가 달라집니다.-
sortAttributesbooleanfalseelement.attributes를 읽는 script가 변경된 순서를 확인하게 됩니다.---
sortTokenListsbooleanfalseclassName이나 rel을 읽는 script가 변경된 순서를 확인하게 됩니다.---

webpack.config.js

export default {
  mode: "production",
  experiments: { html: true, css: true },
  optimization: {
    minimize: {
      html: {
        collapseWhitespace: "smart",
        comments: /^!/,
        removeImpliedTags: "all",
        removeRedundantAttributes: "smart",
        removeEmptyAttributes: true,
        removeEmptyElements: true,
        mergeStyles: true,
        sortAttributes: true,
        sortTokenLists: true,
      },
    },
  },
};

Magic comments

태그 바로 앞에 있는 HTML 주석은 해당 태그만 설정합니다.

주석효과
<!-- webpackIgnore: true -->태그의 URL을 변경하지 않습니다.
<!-- webpackInline: true -->참조한 청크를 페이지에 인라인으로 삽입합니다(false는 제외).
<!-- webpackPreload: true -->다음 참조 애셋의 hint를 preload로 설정합니다.
<!-- webpackPrefetch: true -->hint를 prefetch로 설정합니다.
<!-- webpackFetchPriority: "high" -->hint의 fetchpriority를 설정합니다.

hint 주석은 urlHints 규칙과 같은 필드를 설정하며 해당 규칙보다 우선합니다. 엔트리 포인트별로 리졸브된 hint는 추출된 페이지의 <head>에 <link> 태그로 내보내고 stats.entrypoints[name].resourceHints를 통해 노출합니다.

<!doctype html>
<html lang="en">
  <head>
    <!-- webpackPreload: true -->
    <link rel="stylesheet" href="./critical.css" />
  </head>
  <body>
    <!-- webpackIgnore: true -->
    <script src="https://cdn.example.com/analytics.js"></script>

    <!-- webpackInline: true -->
    <script src="./bootstrap.js"></script>

    <!-- webpackPrefetch: true -->
    <!-- webpackFetchPriority: "low" -->
    <img src="./below-the-fold.avif" alt="" />
  </body>
</html>

Popular examples

Single-page app with hashed assets

webpack.config.js

export default {
  mode: "production",
  entry: "./src/index.html",
  output: {
    filename: "js/[name].[contenthash].js",
    cssFilename: "css/[name].[contenthash].css",
    assetModuleFilename: "assets/[name].[contenthash][ext]",
    htmlFilename: "[name].html",
    clean: true,
  },
  experiments: { html: true, css: true },
};

Inline the runtime chunk

webpack.config.js

export default {
  mode: "production",
  entry: { main: "./src/main.js" },
  optimization: { runtimeChunk: "single" },
  output: {
    html: {
      inline: [/^runtime/],
    },
  },
  experiments: { html: true, css: true },
};

runtime을 인라인으로 삽입하면 blocking 요청 하나가 제거됩니다. inline: 'style'은 모든 CSS 청크에 같은 작업을 수행합니다.

A page per locale

webpack.config.js

import { fileURLToPath } from "node:url";

const locales = ["en", "de", "fr"];
const greetings = { en: "Hello", de: "Hallo", fr: "Bonjour" };

export default locales.map((locale) => ({
  name: locale,
  entry: { [locale]: "./src/index.html" },
  output: {
    path: fileURLToPath(new URL(`./dist/${locale}`, import.meta.url)),
    htmlFilename: "index.html",
  },
  module: {
    parser: {
      html: {
        template: (source) =>
          source
            .replaceAll("{{lang}}", locale)
            .replaceAll("{{greeting}}", greetings[locale]),
      },
    },
  },
  experiments: { html: true, css: true },
}));

Strict CSP with hashed inline scripts

webpack.config.js

export default {
  mode: "production",
  entry: "./src/index.html",
  output: {
    crossOriginLoading: "anonymous",
    html: {
      integrity: true,
      csp: true,
    },
  },
  experiments: { html: true, css: true },
};

csp: true는 엄격한 기본값(script-src 'self', style-src 'self', object-src 'none', base-uri 'self')을 기록하고 각 인라인 <script>/<style>에 sha256 hash를 추가하여 사용자가 작성한 인라인 코드가 계속 실행되도록 합니다.

An installable PWA

webpack.config.js

export default {
  entry: { main: "./src/main.js" },
  output: {
    html: {
      title: "My App",
      meta: { viewport: "width=device-width, initial-scale=1" },
      favicon: "./src/icon.svg",
      manifest: {
        name: "My App",
        short_name: "App",
        start_url: "/",
        display: "standalone",
        background_color: "#ffffff",
        icons: [
          { src: "./src/icon-192.png", sizes: "192x192" },
          { src: "./src/icon-512.png", sizes: "512x512" },
        ],
      },
    },
  },
  experiments: { html: true },
};

service worker 부분은 Progressive Web Application을 참고하세요.

HTML partials rendered at runtime

src/index.js

import card from "./card.partial.html";

document.querySelector("#list").insertAdjacentHTML("beforeend", card);

webpack.config.js

export default {
  experiments: { html: true },
  module: {
    rules: [
      {
        test: /\.partial\.html$/i,
        type: "html",
        parser: { as: "template" },
        generator: { extract: false },
      },
    ],
  },
};

Lazy-loaded page fragments

const template = await import("./modal.partial.html");

document.body.insertAdjacentHTML("beforeend", template.default);

fragment 자체의 <img>와 인라인 <style>은 static import와 같은 방식으로 번들링 및 리졸브되며 dynamic import가 실행될 때만 가져옵니다.

Development server

webpack.config.js

export default {
  mode: "development",
  entry: "./src/index.html",
  devServer: {
    hot: true,
  },
  experiments: { html: true, css: true },
};

내보낸 페이지는 빌드 출력에서 제공되므로 static index.html과 동기화할 template이 없으며 페이지, style, script의 변경 사항이 모두 hot apply됩니다.

Limitations

experiments.html은 명시적으로 활성화해야 하는 기능이므로 광범위하게 도입하기 전에 테스트하세요.

  • webpack v6의 기본 기능이 되기 전까지 API와 동작이 계속 변경될 수 있습니다.
  • html-webpack-plugin과 동일한 기능을 제공하기 위한 작업이 진행 중입니다. chunksSortMode, xhtml, showErrors, cache에 대응하는 기능은 없으며 template engine loader(pug-loader, ejs-loader 등)는 그대로 재사용되지 않고 동기 template hook으로 대체됩니다.
  • <noscript> 콘텐츠를 마크업으로 파싱하는 html-loader의 sources.scriptingEnabled에 대응하는 네이티브 설정은 없습니다.
  • HMR이 활성화된 동안 HTML 모듈에서는 Module concatenation이 비활성화됩니다. 각 모듈에 자체 module.hot scope가 필요하기 때문입니다.

Further reading

Edit this page·
« Previous
Native CSS

1 Contributor

alexander-akait