Printable

Guides

이 섹션에는 webpack이 제공하는 다양한 도구와 기능을 이해하고 마스터하기 위한 가이드가 포함되어 있습니다. 첫 번째는 시작하기를 안내하는 간단한 가이드입니다.

가이드는 계속 진행할수록 더 깊이있는 내용을 다룹니다. 대부분은 시작점 역할을 하고, 완료 후에는 실제 문서를 보다 편안하게 살펴볼 수 있습니다.

Getting Started

애플리케이션에 맞춤형 빌드 파이프라인이 필요하다면 webpack은 좋은 선택입니다. JavaScript 모듈 번들링, 애셋 처리, 로더와 플러그인 통합, 환경별 출력 구성까지 세밀하게 제어할 수 있습니다. 스크립트가 한두 개뿐인 아주 작은 페이지라면 처음에는 번들러가 필요 없을 수도 있지만, 공통 의존성, npm 패키지, 애셋, 프로덕션 빌드가 포함된 애플리케이션이라면 webpack이 전체 조립 과정을 명시적으로 제어할 수 있게 해줍니다.

Webpack은 JavaScript 모듈을 효율적으로 컴파일하는 데 사용됩니다. 설치가 완료되면 CLI 또는 API를 사용하여 webpack과 상호 작용할 수 있습니다. Webpack을 처음 사용하시는 분이라면 핵심 개념이 비교를 읽어보시고, 커뮤니티에 나와 있는 다른 도구들보다 webpack을 사용하는 이유를 알아보세요.

Quick Start (Minimal Working Example)

Webpack 프로젝트를 빠르게 구축하고 실행하고 싶다면 create-webpack-app으로 스캐폴딩하는 것이 가장 쉬운 방법입니다.

npx create-webpack-app webpack-demo
cd webpack-demo

Basic Setup

먼저 디렉터리를 생성합니다. 그 다음 npm을 초기화하고, webpack을 로컬로 설치한 후 webpack-cli(커맨드-라인에서 webpack을 실행할 때 사용되는 도구)를 설치합니다.

# Run the commands for one package manager only.

mkdir webpack-demo
cd webpack-demo

# npm
npm init -y
npm install webpack webpack-cli --save-dev

# yarn
yarn init -y
yarn add webpack webpack-cli --dev

# pnpm
pnpm init
pnpm add webpack webpack-cli -D

가이드 전반에 걸쳐 diff 블록을 사용하여 디렉터리, 파일 코드의 변경을 보여줍니다. 예를 들면,

+ 이것은 코드에 복사할 새로운 라인 입니다.
- 그리고 이것은 코드에서 삭제될 라인 입니다.
  그리고 이것은 손대지 말아야 할 라인 입니다.

이제 다음의 디렉터리 구조와 파일, 콘텐츠를 생성합니다.

project

  webpack-demo
  ├── package.json
  ├── package-lock.json
+ ├── index.html
+ └── src/
+    └── index.js

src/index.js

function component() {
  const element = document.createElement("div");

  // 이 라인이 동작하려면 현재 스크립트를 통해 포함된 Lodash가 필요합니다.
  element.innerHTML = _.join(["Hello", "webpack"], " ");

  return element;
}

document.body.appendChild(component());

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Getting Started</title>
    <script src="https://unpkg.com/lodash@4.17.21"></script>
  </head>
  <body>
    <script src="./src/index.js"></script>
  </body>
</html>

또한 package.json 파일을 수정해 패키지를 private으로 표시하고 main 항목을 제거해야 합니다. 이렇게 하면 코드가 실수로 배포되는 일을 방지할 수 있습니다.

그리고 "type": "module"도 추가해 Node.js가 이 프로젝트의 .js 파일을 ES 모듈로 처리하도록 합니다. 이 설정은 이후의 Node.js 스크립트와 webpack 설정 파일까지 프로젝트 전반에 적용됩니다. Node의 기본 CommonJS 동작을 유지하고 싶다면 "type": "module"을 생략하고, 이 가이드 뒤쪽의 설정은 importexport default 대신 require(...)module.exports로 작성하면 됩니다.

package.json

 {
   "name": "webpack-demo",
   "version": "1.0.0",
   "description": "",
-  "main": "index.js",
+  "private": true,
+  "type": "module",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "keywords": [],
   "author": "",
   "license": "MIT",
   "devDependencies": {
     "webpack": "^5.105.0",
     "webpack-cli": "^7.0.0"
   }
 }

이 예시에서는 <script> 태그 사이에 암시적인 의존성이 있습니다. index.js 파일은 실행되기 전에 페이지에 lodash가 포함되어 있어야 합니다. 즉 전역 변수 _에 암묵적으로 의존하게 되므로, 스크립트 실행 순서가 중요해지고 유지보수도 어려워집니다.

이러한 방식으로 JavaScript 프로젝트를 관리하는 것은 문제가 있습니다.

  • 해당 스크립트가 외부 라이브러리에 의존한다는 것이 명확하지 않습니다.
  • 의존성을 잃어버렸거나 잘못된 순서로 포함되었으면 애플리케이션이 제대로 작동하지 않습니다.
  • 의존성이 포함되었지만 사용되지 않는 경우에도 브라우저는 필요 없는 코드를 강제로 다운로드합니다.

Webpack은 의존성을 명시적으로 선언하고 함께 번들링함으로써 이러한 문제를 해결합니다. 덕분에 전역 변수에 의존하지 않아도 되고, 스크립트도 올바른 순서로 실행되도록 보장할 수 있습니다.

Creating a Bundle

먼저 디렉터리 구조를 약간 수정하여 "배포" 코드(./dist)를 "소스" 코드(./src)와 분리합니다. "소스" 코드는 우리가 작성하고 편집하는 코드입니다. "배포" 코드는 빌드 과정을 통해 최소화하고 최적화되어 궁극적으로 브라우저에서 로드될 출력물 입니다. 다음과 같이 디렉터리 구조를 변경합니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
+  ├── /dist
+  │   └── index.html
-  ├── index.html
   └── /src
       └── index.js

dist 디렉터리는 빌드 결과물이 놓이는 곳이므로, 실제 프로젝트에서는 보통 그 안의 파일을 직접 수정하지 않습니다. 여기서는 브라우저가 첫 번째 번들을 불러올 수 있도록 임시 발판 역할로 index.htmldist로 옮겨 두는 것입니다. 이후 다른 가이드에서는 index.html을 수동으로 수정하지 않고 자동 생성하게 됩니다. 그 단계까지 가면 dist 디렉터리를 비우고 내부 파일을 모두 다시 생성해도 안전합니다.

lodash의 의존성을 index.js와 함께 번들링 하려면, 라이브러리를 로컬에서 설치해야 합니다.

# Run the command for one package manager only.

# npm
npm install lodash

# yarn
yarn add lodash

# pnpm
pnpm add lodash

지금부터 스크립트로 lodash를 가져오겠습니다.

src/index.js

+import _ from 'lodash';
+
 function component() {
   const element = document.createElement('div');

-  // 이 라인이 동작하려면 현재 스크립트를 통해 포함된 Lodash가 필요합니다.
+  // 이제 Lodash를 스크립트로 가져왔습니다.
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

   return element;
 }

 document.body.appendChild(component());

이제 스크립트로 번들링 할 것이므로 index.html을 업데이트해야 합니다. 현재 import한 lodash <script>를 삭제하고 원래의 ./src 파일 대신 다른 <script> 태그로 번들을 로드하도록 수정합니다.

dist/index.html

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
     <title>Getting Started</title>
-    <script src="https://unpkg.com/lodash@4.17.21"></script>
   </head>
   <body>
-    <script src="./src/index.js"></script>
+    <script src="main.js"></script>
   </body>
 </html>

이 설정에서 index.js는 명시적으로 lodash가 있어야 하며, 이것을 _에 바인딩합니다.(전역 스코프의 오염 없음) 모듈에 필요한 의존성을 명시함으로써 webpack은 이 정보를 사용하여 디펜던시 그래프를 만들 수 있습니다. 그런 다음 그래프를 사용하여 스크립트가 올바른 순서로 실행되는 최적화된 번들을 생성합니다.

자, 그럼 프로젝트 루트 디렉토리에서 npx webpack 명령어를 실행해 보겠습니다. webpack이 로컬에 설치되어 있다면 npxnode_modules/.bin에 있는 바이너리를 실행하고, 그렇지 않다면 다운로드하여 실행할 수 있습니다. 이 명령어는 src/index.js에 있는 스크립트를 진입점으로 사용하여 dist/main.js출력으로 생성합니다.

# Run the command for one package manager only.

# npm
npx webpack

# yarn
yarn webpack

# pnpm
pnpm exec webpack

[webpack-cli] Compilation finished
asset main.js 69.3 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 1851 ms

브라우저에서 dist 디렉터리의 index.html을 열어보세요. 모든 것이 제대로 되었다면 'Hello webpack' 텍스트가 표시될 것입니다.

Modules

importexportES2015에서 표준화되었습니다. 현재 대부분의 브라우저가 이를 지원하지만, 새 문법을 인식하지 못하는 브라우저도 일부 존재합니다. 그래도 걱정할 필요는 없습니다. webpack이 기본적으로 이를 지원합니다.

내부적으로 webpack은 모듈 그래프를 분석한 뒤, 브라우저가 올바른 순서로 로드할 수 있는 코드로 모듈을 번들링합니다. importexport 같은 모듈 문법을 처리하며, 그 외 다양한 모듈 문법도 지원합니다. 자세한 내용은 Module API를 참고하세요.

webpack은 importexport 문 이외는 코드를 변경하지 않습니다. 다른 ES2015 기능을 사용한다면 webpack의 로더 시스템Babel트랜스파일러로 사용해야 합니다.

Using a Configuration

버전 4부터 webpack은 어떠한 설정도 필요하지 않습니다. 하지만 대부분의 프로젝트는 좀 더 복잡한 설정이 필요하므로 webpack에서 설정 파일을 제공합니다. 이것은 터미널에서 많은 명령어를 수동으로 입력하는 것보다 훨씬 효율적입니다. 다음과 같이 생성해 보겠습니다.

Webpack 설정 파일은 CommonJS 또는 ECMAScript 모듈 방식으로 작성할 수 있습니다. 아래 예제에서는 최신 ESM 문법을 사용합니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
+  ├── webpack.config.js
   ├── /dist
   │   └── index.html
   └── /src
       └── index.js

webpack.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

// import.meta.dirname을 기본적으로 지원하지 않는 Node.js 버전에서는,
// import.meta.url에서 __dirname을 파생합니다.
// (Node 20.11 이상 버전에서는 import.meta.dirname과 import.meta.filename을 지원합니다.)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

이제 새로운 설정 파일을 이용하여 다시 빌드를 실행해 보세요.

# Run the command for one package manager only.

# npm
npx webpack --config webpack.config.js

# yarn
yarn webpack --config webpack.config.js

# pnpm
pnpm exec webpack --config webpack.config.js

[webpack-cli] Compilation finished
asset main.js 69.3 KiB [compared for emit] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 1934 ms

설정 파일은 단순한 CLI 사용보다 훨씬 많은 유연성을 제공합니다. 로더의 규칙, 플러그인, 해석 옵션 및 기타 여러 향상된 기능을 지정할 수 있습니다. 더 자세한 것은 설정 문서를 참고하세요.

NPM Scripts

CLI에서 webpack의 로컬 사본을 실행하기 위해 약간의 단축 명령어를 설정 할 수 있습니다. npm script를 추가하여 package.json을 수정해 보겠습니다.

package.json

 {
   "name": "webpack-demo",
   "version": "1.0.0",
   "description": "",
   "private": true,
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "build": "webpack"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "devDependencies": {
     "webpack": "^5.105.0",
     "webpack-cli": "^7.0.0"
   },
   "dependencies": {
     "lodash": "^4.17.21"
   }
 }

이제 이전에 사용한 npx 명령 대신 npm run build 명령을 사용할 수 있습니다. scripts에서는 npx와 동일한 방식으로 로컬에서 설치된 npm 패키지를 이름으로 참조할 수 있습니다. 이 규칙은 모든 컨트리뷰터가 동일한 공통의 스크립트 세트를 사용할 수 있도록 하므로 대부분의 npm 기반 프로젝트에서 표준입니다.

이제 다음 명령을 실행하고 스크립트의 별칭이 작동하는지 확인하세요.

# Run the command for one package manager only.

# npm
npm run build

# yarn
yarn build

# pnpm
pnpm run build
...

[webpack-cli] Compilation finished
asset main.js 69.3 KiB [compared for emit] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 1940 ms

Conclusion

이제 기본적인 빌드를 완료했으니 다음 가이드인 Asset Management로 넘어가 웹팩을 사용하여 이미지와 폰트 같은 자산을 관리하는 방법을 배우세요. 이 시점에서 프로젝트는 다음과 같아야 합니다.

project

webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 │   ├── main.js
 │   └── index.html
 ├── /src
 │   └── index.js
 └── /node_modules

Webpack 디자인에 대해 자세히 알아보고 싶으면 basic conceptsconfiguration 페이지를 확인하세요. 또한 API에서 webpack이 제공하는 다양한 인터페이스를 자세히 살펴봅니다.

Asset Management

처음부터 가이드를 따라왔다면 이제 "Hello webpack"을 표시하는 작은 프로젝트가 생성되었을 것입니다. 이제 이미지와 같은 다른 애셋을 통합하고, 애셋이 어떻게 처리되는지 살펴보겠습니다.

webpack 이전에 프런트엔드 개발자는 gruntgulp 같은 도구를 사용하여 애셋을 처리하고 /src 폴더에서 /dist 또는 /build 디렉터리로 옮겼습니다. JavaScript 모듈에도 동일한 아이디어가 사용되었지만, webpack과 같은 도구는 모든 의존성을 동적으로 번들합니다. (디펜던시 그래프로 알려진 것을 생성합니다). 이것이 좋은 이유는 이제 모든 모듈이 의존성을 명확하게 명시하고 사용하지 않는 모듈을 번들에서 제외할 수 있기 때문입니다.

webpack의 가장 멋진 기능 중 하나는 JavaScript 외에도 로더 또는 내장 애셋 모듈이 지원하는 다른 유형의 파일도 포함 할 수 있다는 것입니다. 즉, 위에 나열된 JavaScript의 이점(예: 명시적 의존성)을 웹 사이트 또는 웹 앱을 만드는 데 사용한 모든 것에 적용할 수 있습니다. 이미 설정에 익숙 할 수 있는 CSS부터 시작해 보겠습니다.

Setup

시작하기 전에 프로젝트를 조금 변경해 보겠습니다.

dist/index.html

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
-    <title>Getting Started</title>
+    <title>Asset Management</title>
   </head>
   <body>
-    <script src="main.js"></script>
+    <script src="bundle.js"></script>
   </body>
 </html>

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
-    filename: 'main.js',
+    filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

Loading CSS

JavaScript 모듈 내에서 CSS 파일을 import 하려면 style-loadercss-loader를 설치하고 module 설정에 추가해야 합니다.

npm install --save-dev style-loader css-loader

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  module: {
+    rules: [
+      {
+        test: /\.css$/i,
+        use: ['style-loader', 'css-loader'],
+      },
+    ],
+  },
 };

모듈 로더는 체인으로 연결할 수 있습니다. 체인의 각 로더는 처리 대상 리소스에 변환을 적용하며, 실행 순서는 역순(오른쪽에서 왼쪽)입니다.

예를 들어 다음과 같은 규칙이 있다고 해보겠습니다.

export default {
  module: {
    rules: [
      {
        test: /\.scss$/i,
        use: ["postcss-loader", "sass-loader"],
      },
    ],
  },
};

use 배열에서는 postcss-loadersass-loader보다 앞에 있지만, webpack은 먼저 sass-loader를 실행해 Sass를 CSS로 변환하고 그 결과에 다시 postcss-loader를 적용합니다.

이 순서가 올바르게 유지되지 않으면 webpack에서 오류가 발생할 수 있습니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
   ├── webpack.config.js
   ├── /dist
   │   ├── bundle.js
   │   └── index.html
   ├── /src
+  │   ├── style.css
   │   └── index.js
   └── /node_modules

src/style.css

.hello {
  color: red;
}

src/index.js

 import _ from 'lodash';
+import './style.css';

 function component() {
   const element = document.createElement('div');

   // 이제 이 스크립트가 Lodash를 import합니다.
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  element.classList.add('hello');

   return element;
 }

 document.body.appendChild(component());

이제 빌드 커맨드를 실행합니다.

$ npm run build

...
[webpack-cli] Compilation finished
asset bundle.js 72.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 539 KiB
  modules by path ./node_modules/ 538 KiB
    ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
    ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
    ./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
  modules by path ./src/ 965 bytes
    ./src/index.js + 1 modules 639 bytes [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./src/style.css 326 bytes [built] [code generated]
webpack 5.x.x compiled successfully in 2231 ms

브라우저에서 dist/index.html을 다시 열면 이제 Hello webpack이 빨간색으로 표시됩니다. webpack이 무엇을 했는지 확인하려면 페이지를 검사하여 head 태그를 살펴보세요. (<style>태그는 JavaScript를 통해 동적으로 생성되며 결과를 표시하지 않으므로 페이지 소스를 확인하지 마세요) head 태그에 index.js에서 가져온 스타일 블록이 포함되어 있을 것입니다.

대부분의 경우 필수겠지만, 이제 프로덕션에서 로드 시간을 단축하기 위해 css를 압축 할 수 있습니다. 또한 생각할 수 있는 거의 모든 종류의 CSS 로더가 존재합니다. 몇 가지 예를 들면 postcss, sassless 등이 있습니다.

Loading Images

이제 CSS는 가져왔는데, 배경이나 아이콘과 같은 이미지는 어떻게 할까요? 이미지도 webpack 5부터 내장된 Asset Modules를 사용하여 시스템에 쉽게 통합할 수 있습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
+      {
+        test: /\.(png|svg|jpg|jpeg|gif)$/i,
+        type: 'asset/resource',
+      },
     ],
   },
 };

이제 import myImage from './my-image.png'를 사용하면 해당 이미지가 처리되어 output 디렉터리에 추가됩니다. 그리고 MyImage 변수는 이미지의 최종 URL을 포함합니다. 위와 같이 css-loader를 사용하면 CSS 내의 url('./my-image.png')에도 유사한 프로세스가 적용됩니다. 로더는 이것이 로컬 파일임을 인식하고 './my-image.png' 경로를 output 디렉터리에 있는 이미지의 최종 경로로 변경합니다. html-loader<img src="./my-image.png" />를 동일한 방식으로 처리합니다.

이제 프로젝트에 이미지를 추가하고 어떻게 작동하는지 살펴볼까요? 원하는 이미지를 아무거나 사용해도 좋습니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
   ├── webpack.config.js
   ├── /dist
   │   ├── bundle.js
   │   └── index.html
   ├── /src
+  │   ├── icon.png
   │   ├── style.css
   │   └── index.js
   └── /node_modules

src/index.js

 import _ from 'lodash';
 import './style.css';
+import Icon from './icon.png';

 function component() {
   const element = document.createElement('div');

   // Lodash, now imported by this script
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.classList.add('hello');

+  // 원래 있던 div 에 이미지를 추가합니다.
+  const myIcon = new Image();
+  myIcon.src = Icon;
+
+  element.appendChild(myIcon);
+
   return element;
 }

 document.body.appendChild(component());

src/style.css

 .hello {
   color: red;
+  background: url('./icon.png');
 }

새 빌드를 만들고 index.html 파일을 다시 엽니다.

$ npm run build

...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
asset bundle.js 73.4 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 540 KiB (javascript) 9.88 KiB (asset)
  modules by path ./node_modules/ 539 KiB
    modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB
      ./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
      ./node_modules/css-loader/dist/runtime/getUrl.js 830 bytes [built] [code generated]
    ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
    ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
  modules by path ./src/ 1.45 KiB (javascript) 9.88 KiB (asset)
    ./src/index.js + 1 modules 794 bytes [built] [code generated]
    ./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./src/style.css 648 bytes [built] [code generated]
webpack 5.x.x compiled successfully in 1972 ms

모든 것이 순조롭게 진행되었다면 이제 아이콘이 반복해서 배경으로 표시되고, Hello webpack 텍스트 옆에 img 요소가 보이게 됩니다. 이 요소를 살펴보면 실제 파일 이름이 29822eaa871e8eadeaa4.png와 같이 변경된 것을 볼 수 있습니다. 이것은 webpack이 src 폴더에서 파일을 찾아서 처리했음을 의미합니다!

Loading Fonts

그렇다면 폰트와 같은 다른 애셋은 어떨까요? 애셋 모듈은 로드한 모든 파일을 가져와 빌드 디렉터리로 내보냅니다. 즉, 폰트를 포함한 모든 종류의 파일에 사용할 수 있습니다. 폰트 파일을 처리하도록 webpack.config.js를 업데이트해 보겠습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
+      {
+        test: /\.(woff|woff2|eot|ttf|otf)$/i,
+        type: 'asset/resource',
+      },
     ],
   },
 };

프로젝트에 몇 개의 폰트 파일을 추가합니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
   ├── webpack.config.js
   ├── /dist
   │   ├── bundle.js
   │   └── index.html
   ├── /src
+  │   ├── my-font.woff
+  │   ├── my-font.woff2
   │   ├── icon.png
   │   ├── style.css
   │   └── index.js
   └── /node_modules

로더를 설정하고 폰트가 맞는 위치에 있으면 @font-face 선언을 통해 적용 할 수 있습니다. 로컬 url(...) 지시문은 이미지와 마찬가지로 webpack에서 골라냅니다.

src/style.css

+@font-face {
+  font-family: 'MyFont';
+  src: url('./my-font.woff2') format('woff2'),
+    url('./my-font.woff') format('woff');
+  font-weight: 600;
+  font-style: normal;
+}
+
 .hello {
   color: red;
+  font-family: 'MyFont';
   background: url('./icon.png');
 }

이제 새 빌드를 실행하고 webpack이 폰트를 처리했는지 살펴보겠습니다.

$ npm run build

...
[webpack-cli] Compilation finished
assets by status 9.88 KiB [cached] 1 asset
assets by info 33.2 KiB [immutable]
  asset 55055dbfc7c6a83f60ba.woff 18.8 KiB [emitted] [immutable] [from: src/my-font.woff] (auxiliary name: main)
  asset 8f717b802eaab4d7fb94.woff2 14.5 KiB [emitted] [immutable] [from: src/my-font.woff2] (auxiliary name: main)
asset bundle.js 73.7 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 541 KiB (javascript) 43.1 KiB (asset)
  javascript modules 541 KiB
    modules by path ./node_modules/ 539 KiB
      modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB 2 modules
      ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
      ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
    modules by path ./src/ 1.98 KiB
      ./src/index.js + 1 modules 794 bytes [built] [code generated]
      ./node_modules/css-loader/dist/cjs.js!./src/style.css 1.21 KiB [built] [code generated]
  asset modules 126 bytes (javascript) 43.1 KiB (asset)
    ./src/icon.png 42 bytes (javascript) 9.88 KiB (asset) [built] [code generated]
    ./src/my-font.woff2 42 bytes (javascript) 14.5 KiB (asset) [built] [code generated]
    ./src/my-font.woff 42 bytes (javascript) 18.8 KiB (asset) [built] [code generated]
webpack 5.x.x compiled successfully in 2142 ms

dist/index.html을 다시 열고 Hello webpack 텍스트가 새 폰트로 변경되었는지 확인합니다. 모든 것이 잘되었다면, 변경된 폰트를 확인할 수 있을 것입니다.

Loading Data

로드할 수 있는 또 다른 유용한 애셋은 JSON 파일, CSV, TSV 및 XML과 같은 데이터입니다. JSON 지원은 기본으로 내장되어 있으며 NodeJS와 유사합니다. 즉, 기본적으로 import Data from './data.json'이 동작합니다. CSV, TSV 및 XML을 가져오려면 csv-loaderxml-loader를 사용할 수 있습니다. 세 가지 모두 로드해 보겠습니다.

npm install --save-dev csv-loader xml-loader

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
       {
         test: /\.(woff|woff2|eot|ttf|otf)$/i,
         type: 'asset/resource',
       },
+      {
+        test: /\.(csv|tsv)$/i,
+        use: ['csv-loader'],
+      },
+      {
+        test: /\.xml$/i,
+        use: ['xml-loader'],
+      },
     ],
   },
 };

프로젝트에 데이터 파일을 추가합니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
   ├── webpack.config.js
   ├── /dist
   │   ├── bundle.js
   │   └── index.html
   ├── /src
+  │   ├── data.xml
+  │   ├── data.csv
   │   ├── my-font.woff
   │   ├── my-font.woff2
   │   ├── icon.png
   │   ├── style.css
   │   └── index.js
   └── /node_modules

src/data.xml

<?xml version="1.0" encoding="UTF-8"?>
<note>
  <to>Mary</to>
  <from>John</from>
  <heading>Reminder</heading>
  <body>Call Cindy on Tuesday</body>
</note>

src/data.csv

to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you

이제 네 가지 데이터 유형(JSON, CSV, TSV, XML) 중 하나를 import 할 수 있으며, 가져오는 Data 변수에는 파싱된 JSON이 포함되어 쉽게 사용할 수 있습니다.

src/index.js

 import _ from 'lodash';
 import './style.css';
 import Icon from './icon.png';
+import Data from './data.xml';
+import Notes from './data.csv';

 function component() {
   const element = document.createElement('div');

   // Lodash, now imported by this script
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.classList.add('hello');

   // 기존 div에 이미지를 추가합니다.
   const myIcon = new Image();
   myIcon.src = Icon;

   element.appendChild(myIcon);

+  console.log(Data);
+  console.log(Notes);
+
   return element;
 }

 document.body.appendChild(component());

npm run build 명령을 다시 실행하고 dist/index.html을 엽니다. 개발자 도구의 콘솔에 가져온 데이터가 기록되는 것을 볼 수 있습니다!

// 경고 없음
import data from "./data.json";
// 스펙에서 허용하지 않으므로 경고가 노출됨
import { foo } from "./data.json";

Customize parser of JSON modules

특정 webpack 로더 대신 [커스텀 파서](/configuration/modules# ruleparserparse)를 사용하여 toml, yaml 또는 json5 파일을 JSON 모듈로 가져올 수 있습니다.

src 폴더에 data.toml, data.yamldata.json5 파일이 있다고 가정해 보겠습니다.

src/data.toml

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z

src/data.yaml

title: YAML Example
owner:
  name: Tom Preston-Werner
  organization: GitHub
  bio: |-
    GitHub Cofounder & CEO
    Likes tater tots and beer.
  dob: 1979-05-27T07:32:00.000Z

src/data.json5

{
  // comment
  title: "JSON5 Example",
  owner: {
    name: "Tom Preston-Werner",
    organization: "GitHub",
    bio: "GitHub Cofounder & CEO\n\
Likes tater tots and beer.",
    dob: "1979-05-27T07:32:00.000Z",
  },
}

먼저 toml, yamljsjson5 패키지를 설치합니다.

npm install toml yamljs json5 --save-dev

그리고 webpack 설정에 추가합니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import toml from 'toml';
+import yaml from 'yamljs';
+import json5 from 'json5';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: /\.css$/i,
         use: ['style-loader', 'css-loader'],
       },
       {
         test: /\.(png|svg|jpg|jpeg|gif)$/i,
         type: 'asset/resource',
       },
       {
         test: /\.(woff|woff2|eot|ttf|otf)$/i,
         type: 'asset/resource',
       },
       {
         test: /\.(csv|tsv)$/i,
         use: ['csv-loader'],
       },
       {
         test: /\.xml$/i,
         use: ['xml-loader'],
       },
+      {
+        test: /\.toml$/i,
+        type: 'json',
+        parser: {
+          parse: toml.parse,
+        },
+      },
+      {
+        test: /\.yaml$/i,
+        type: 'json',
+        parser: {
+          parse: yaml.parse,
+        },
+      },
+      {
+        test: /\.json5$/i,
+        type: 'json',
+        parser: {
+          parse: json5.parse,
+        },
+      },
     ],
   },
 };

src/index.js

 import _ from 'lodash';
 import './style.css';
 import Icon from './icon.png';
 import Data from './data.xml';
 import Notes from './data.csv';
+import toml from './data.toml';
+import yaml from './data.yaml';
+import json from './data.json5';
+
+console.log(toml.title); // output `TOML Example`
+console.log(toml.owner.name); // output `Tom Preston-Werner`
+
+console.log(yaml.title); // output `YAML Example`
+console.log(yaml.owner.name); // output `Tom Preston-Werner`
+
+console.log(json.title); // output `JSON5 Example`
+console.log(json.owner.name); // output `Tom Preston-Werner`

 function component() {
   const element = document.createElement('div');

   // Lodash, now imported by this script
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
   element.classList.add('hello');

   // Add the image to our existing div.
   const myIcon = new Image();
   myIcon.src = Icon;

   element.appendChild(myIcon);

   console.log(Data);
   console.log(Notes);

   return element;
 }

 document.body.appendChild(component());

npm run build 명령을 다시 실행하고 dist/index.html을 확인합니다. 가져온 데이터가 콘솔에 기록되는 것을 볼 수 있습니다!

Global Assets

위에서 언급 한 모든 것 중 가장 멋진 점은 이러한 방식으로 애셋을 로드하면 모듈과 애셋을 보다 직관적인 방식으로 그룹화할 수 있다는 것입니다. 모든 것을 포함한 글로벌 /assets 디렉터리에 의존하는 대신 애셋을 사용하는 코드와 그룹화할 수 있습니다. 예를 들어 다음과 같은 구조가 유용할 수 있습니다.

- ├── /assets
+ └── /components
+     └── /my-component
+         ├── index.jsx
+         ├── index.css
+         ├── icon.svg
+         └── img.png

이러한 설정은 밀접하게 연결된 모든 것이 함께 있기 때문에 코드를 다른 곳에 훨씬 더 쉽게 적용할 수 있도록 합니다. 다른 프로젝트에서 /my-component를 사용한다고 가정해 봅시다. 간단히 복사하거나 다른 프로젝트의 /components 디렉터리로 옮기면 됩니다. 외부 의존성을 설치하고 설정에 동일한 로더가 정의되어있는 한 아무 문제가 없습니다.

그러나 예전 방식을 고수하고 있거나 여러 컴포넌트(뷰, 템플릿, 모듈 등) 간에 공유되는 애셋이 있다고 가정해 보겠습니다. 이러한 애셋을 기본 디렉터리에 저장하는 것도 가능하며 aliasing을 사용하여 쉽게 import 할 수 있습니다.

Wrapping up

다음 가이드에서는 이 가이드에서 사용한 각기 다른 애셋을 모두 사용하지 않을 것이므로 다음 가이드인 Output Management 준비를 위해 정리를 해 보겠습니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
   ├── webpack.config.js
   ├── /dist
   │   ├── bundle.js
   │   └── index.html
   ├── /src
-  │   ├── data.csv
-  │   ├── data.json5
-  │   ├── data.toml
-  │   ├── data.xml
-  │   ├── data.yaml
-  │   ├── icon.png
-  │   ├── my-font.woff
-  │   ├── my-font.woff2
-  │   ├── style.css
   │   └── index.js
   └── /node_modules

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
-import toml from 'toml';
-import yaml from 'yamljs';
-import json5 from 'json5';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
-  module: {
-    rules: [
-      {
-        test: /\.css$/i,
-        use: ['style-loader', 'css-loader'],
-      },
-      {
-        test: /\.(png|svg|jpg|jpeg|gif)$/i,
-        type: 'asset/resource',
-      },
-      {
-        test: /\.(woff|woff2|eot|ttf|otf)$/i,
-        type: 'asset/resource',
-      },
-      {
-        test: /\.(csv|tsv)$/i,
-        use: ['csv-loader'],
-      },
-      {
-        test: /\.xml$/i,
-        use: ['xml-loader'],
-      },
-      {
-        test: /\.toml$/i,
-        type: 'json',
-        parser: {
-          parse: toml.parse,
-        },
-      },
-      {
-        test: /\.yaml$/i,
-        type: 'json',
-        parser: {
-          parse: yaml.parse,
-        },
-      },
-      {
-        test: /\.json5$/i,
-        type: 'json',
-        parser: {
-          parse: json5.parse,
-        },
-      },
-    ],
-  },
 };

src/index.js

 import _ from 'lodash';
-import './style.css';
-import Icon from './icon.png';
-import Data from './data.xml';
-import Notes from './data.csv';
-import toml from './data.toml';
-import yaml from './data.yaml';
-import json from './data.json5';
-
-console.log(toml.title); // output `TOML Example`
-console.log(toml.owner.name); // output `Tom Preston-Werner`
-
-console.log(yaml.title); // output `YAML Example`
-console.log(yaml.owner.name); // output `Tom Preston-Werner`
-
-console.log(json.title); // output `JSON5 Example`
-console.log(json.owner.name); // output `Tom Preston-Werner`

 function component() {
   const element = document.createElement('div');

-  // Lodash, now imported by this script
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-  element.classList.add('hello');
-
-  // Add the image to our existing div.
-  const myIcon = new Image();
-  myIcon.src = Icon;
-
-  element.appendChild(myIcon);
-
-  console.log(Data);
-  console.log(Notes);

   return element;
 }

 document.body.appendChild(component());

그리고 추가했던 의존성을 제거합니다.

npm uninstall css-loader csv-loader json5 style-loader toml xml-loader yamljs

Next guide

이제 Output Management로 넘어가 보겠습니다.

Further Reading

Output Management

지금까지 모든 애셋을 index.html 파일에 수동으로 포함했습니다. 하지만 애플리케이션이 커지면서 파일 이름에 해시를 사용하거나 다중 번들로 내보내기 시작하면 index.html 파일을 수동으로 관리하기 어렵습니다. 이 때 몇 가지 플러그인으로 이 프로세스를 훨씬 쉽게 관리할 수 있습니다.

Preparation

먼저 프로젝트를 조금 수정해보겠습니다.

project

  webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
+ │   ├── print.js
  └── /node_modules

src/print.js 파일에 로직을 추가합니다.

src/print.js

export default function printMe() {
  console.log("I get called from print.js!");
}

그리고 src/index.js 파일에서 이 함수를 사용합니다.

src/index.js

 import _ from 'lodash';
+import printMe from './print.js';

 function component() {
   const element = document.createElement('div');
+  const btn = document.createElement('button');

   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

+  btn.innerHTML = 'Click me and check the console!';
+  btn.onclick = printMe;
+
+  element.appendChild(btn);
+
   return element;
 }

 document.body.appendChild(component());

webpack이 엔트리를 분할할 수 있도록 dist/index.html 파일도 업데이트해 보겠습니다.

dist/index.html

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
-    <title>Asset Management</title>
+    <title>Output Management</title>
+    <script src="./print.bundle.js"></script>
   </head>
   <body>
-    <script src="bundle.js"></script>
+    <script src="./index.bundle.js"></script>
   </body>
 </html>

이제 설정을 수정합니다. src/print.js를 새 엔트리 포인트(print)로 추가합니다. 그리고 출력 번들 이름이 엔트리 포인트 이름을 기반으로 동적으로 생성되도록 변경합니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
-  entry: './src/index.js',
+  entry: {
+    index: './src/index.js',
+    print: './src/print.js',
+  },
   output: {
-    filename: 'bundle.js',
+    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

npm run build를 실행하고 무엇이 생성되는지 살펴보겠습니다.

...
[webpack-cli] Compilation finished
asset index.bundle.js 69.5 KiB [emitted] [minimized] (name: index) 1 related asset
asset print.bundle.js 316 bytes [emitted] [minimized] (name: print)
runtime modules 1.36 KiB 7 modules
cacheable modules 530 KiB
  ./src/index.js 406 bytes [built] [code generated]
  ./src/print.js 83 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 1996 ms

webpack이 print.bundle.jsindex.bundle.js 파일을 생성하는 것을 볼 수 있습니다. 이 파일은 index.html 파일에도 명시되어 있습니다. 브라우저에서 index.html을 열고 버튼을 클릭하면 어떻게 되는지 확인할 수 있습니다.

그러나 엔트리 포인트 중 하나의 이름을 변경하거나 새 엔트리 포인트를 추가하면 어떻게 될까요? 생성된 번들은 빌드에서 이름이 변경되지만 index.html 파일은 여전히 예전 이름을 참조합니다. HtmlWebpackPlugin을 사용하여 이 문제를 해결해보겠습니다.

Setting up HtmlWebpackPlugin

먼저 플러그인을 설치하고 webpack.config.js 파일을 수정합니다.

npm install --save-dev html-webpack-plugin

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
+import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
+  plugins: [
+    new HtmlWebpackPlugin({
+      title: 'Output Management',
+    }),
+  ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

빌드하기 전에 dist/ 폴더에 이미 index.html이 있더라도 기본적으로 HtmlWebpackPlugin이 자체 index.html 파일을 생성하는 것을 알아두세요. 이는 index.html 파일이 새로 생성된 파일로 대체된다는 의미입니다. npm run build를 실행할 때 어떤 일이 발생하는지 살펴보겠습니다.

...
[webpack-cli] Compilation finished
asset index.bundle.js 69.5 KiB [compared for emit] [minimized] (name: index) 1 related asset
asset print.bundle.js 316 bytes [compared for emit] [minimized] (name: print)
asset index.html 253 bytes [emitted]
runtime modules 1.36 KiB 7 modules
cacheable modules 530 KiB
  ./src/index.js 406 bytes [built] [code generated]
  ./src/print.js 83 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 2189 ms

코드 편집기에서 index.html을 열면 HtmlWebpackPlugin이 완전히 새로운 파일을 생성했으며 모든 번들이 자동으로 추가된 것을 알 수 있습니다.

HtmlWebpackPlugin이 제공하는 모든 기능과 옵션에 대해 더 자세히 알아보려면 HtmlWebpackPlugin 저장소를 확인해 보세요.

Cleaning up the /dist folder

이전 가이드와 코드 예제에서 눈치챘겠지만 /dist 폴더가 상당히 복잡해졌습니다. webpack은 파일을 생성하여 /dist 폴더에 저장하지만, 프로젝트에서 실제로 사용하는 파일이 어떤 건지는 알지 못합니다.

일반적으로 사용하는 파일만 생성되도록 각 빌드 전에 /dist 폴더를 정리하는 것이 좋습니다. output.clean 옵션을 사용하여 처리해보겠습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Output Management',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
+    clean: true,
   },
 };

이제 npm run build를 실행하고 /dist 폴더를 확인해보세요. 모든 것이 잘 되었다면 이제 오래된 파일 없이 빌드에서 생성된 파일만 볼 수 있습니다!

The Manifest

webpack과 플러그인은 어떤 파일이 생성되는 것을 어떻게 "알고 있는지" 궁금할 것입니다. 답은 매니페스트에 있습니다. webpack은 모든 모듈이 출력 번들에 어떻게 매핑되는지 추적합니다. 만약 webpack의 output을 다른 방식으로 관리하는데 관심이 있다면 매니페스트부터 시작하는 것이 좋습니다.

매니페스트 데이터는 ManifestPlugin을 사용하여 쉽게 적용 가능한 json 파일로 추출할 수 있습니다.

프로젝트에서 이 플러그인을 사용하는 방법에 대한 모든 예제를 다루지는 않겠지만 콘셉 페이지캐싱 가이드를 읽어 보면 이것이 장기 캐싱과 어떻게 연결되는지 확인할 수 있습니다.

Conclusion

HTML에 번들을 동적으로 추가하는 방법을 배웠으므로 이제 개발 가이드를 살펴보세요. 또는 심화 항목을 자세히 알아보고 싶다면 코드 스플리팅 가이드를 추천합니다.

Development

가이드를 차례대로 따라왔다면, webpack 기본 사양 중 일부를 확실히 이해하고 있을 것입니다. 계속하기 전 우리의 삶을 좀 더 편안하게 만들 개발 환경 설정을 살펴보겠습니다.

먼저 mode'development' 설정하고 title'Development'로 설정해보겠습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
+  mode: 'development',
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
   plugins: [
     new HtmlWebpackPlugin({
-      title: 'Output Management',
+      title: 'Development',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
 };

Using source maps

webpack이 소스 코드를 번들로 묶을 때, 오류와 경고의 원래 위치를 추적하기 어려울 수 있습니다. 예를 들어, 세 개의 소스 파일(a.js, b.js, 그리고 c.js)을 하나의 번들로 묶고 하나의 소스 파일이 오류가 있는 경우, 스택 추적은 단순히 bundle.js를 가리킵니다. 오류가 발생한 소스 파일을 정확히 알고 싶기 때문에 항상 도움이 되는 것은 아닙니다.

오류와 경고를 쉽게 추적할 수 있도록, JavaScript는 컴파일된 코드를 원래 소스로 매핑하는 소스맵을 제공합니다. b.js에서 오류가 발생한 경우, 소스맵에서 정확히 알려줍니다.

소스맵과 관련하여 사용할 수 있는 다른 옵션이 많이 있습니다. 필요에 따라 설정할 수 있도록 확인하세요.

이 가이드에서는, 프로덕션에는 적합하지 않지만 설명 목적으로 유용한 inline-source-map 옵션을 사용하겠습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   mode: 'development',
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
+  devtool: 'inline-source-map',
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Development',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
 };

이제 디버깅할 내용이 있는지 확인하고, print.js 파일에 오류를 생성해 보겠습니다.

src/print.js

 export default function printMe() {
-  console.log('I get called from print.js!');
+  cosnole.log('I get called from print.js!');
 }

npm run build를 실행하면, 다음과 같이 컴파일됩니다.

...
[webpack-cli] Compilation finished
asset index.bundle.js 1.38 MiB [emitted] (name: index)
asset print.bundle.js 6.25 KiB [emitted] (name: print)
asset index.html 272 bytes [emitted]
runtime modules 1.9 KiB 9 modules
cacheable modules 530 KiB
  ./src/index.js 406 bytes [built] [code generated]
  ./src/print.js 83 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 706 ms

이제 브라우저에서 index.html 파일을 엽니다. 버튼을 클릭하고, 오류가 표시된 콘솔을 확인합니다. 오류는 다음과 같이 표시되어야 합니다.

Uncaught ReferenceError: cosnole is not defined
   at HTMLButtonElement.printMe (print.js:2)

오류에서 오류가 발생 한 파일(print.js)과 줄 번호(2)에 대한 참조도 포함되어 있음을 알 수 있습니다. 이제 문제를 해결하기 위해 어디를 봐야 하는지 정확히 알 수 있습니다.

Choosing a Development Tool

코드를 컴파일할 때마다 npm run build를 수동으로 실행하는 것은 번거롭습니다.

webpack에는 코드가 변경될 때마다 자동으로 컴파일하는 데 도움이 되는 몇 가지 옵션이 있습니다.

  1. webpack의 watch 모드
  2. webpack-dev-server
  3. webpack-dev-middleware

대부분의 경우, webpack-dev-server를 사용하고 싶겠지만, 위의 모든 옵션을 살펴보겠습니다.

Using Watch Mode

webpack이 디펜던시 그래프 내의 모든 파일에서의 변경사항을 "감시"하도록 지시할 수 있습니다. 이런 파일 중 하나가 업데이트되면, 코드가 다시 컴파일되므로 전체 빌드를 수동으로 실행할 필요가 없습니다.

webpack의 watch 모드를 시작하는 npm 스크립트를 추가해 보겠습니다.

package.json

 {
   "name": "webpack-demo",
   "version": "1.0.0",
   "description": "",
   "private": true,
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
+    "watch": "webpack --watch",
     "build": "webpack"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "devDependencies": {
     "html-webpack-plugin": "^5.6.6",
     "webpack": "^5.105.0",
     "webpack-cli": "^7.0.0"
   },
   "dependencies": {
     "lodash": "^4.17.21"
   }
 }

커멘드 라인에서 npm run watch를 실행하고 webpack이 코드를 컴파일하는 방법을 확인하세요. 스크립트가 현재 파일을 감시하고 있기 때문에 커멘드 라인을 종료하지 않은 것을 확인할 수 있습니다.

이제, webpack이 파일을 감시하는 동안, 앞에서 소개한 오류를 제거해 보겠습니다.

src/print.js

 export default function printMe() {
-  cosnole.log('I get called from print.js!');
+  console.log('I get called from print.js!');
 }

이제 파일을 저장하고 터미널 창을 확인하십시오. webpack이 변경된 모듈을 자동으로 재컴파일하는 것을 볼 수 있습니다!

유일한 단점은 변경사항을 확인하려면 브라우저를 새로 고침해야 한다는 것입니다. 이것이 자동으로 된다면 더 좋을 것이므로, webpack-dev-server를 사용해 봅시다.

Using webpack-dev-server

webpack-dev-server는 간단한 웹 서버와 실시간 다시 로딩 기능을 제공합니다. 설정해보겠습니다.

npm install --save-dev webpack-dev-server

설정 파일을 변경하여 개발 서버에 파일을 찾을 위치를 알려줍니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   mode: 'development',
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
   devtool: 'inline-source-map',
+  devServer: {
+    static: './dist',
+  },
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Development',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
+  optimization: {
+    runtimeChunk: 'single',
+  },
 };

이것은 webpack-dev-server에게 dist 디렉터리의 파일을 localhost:8080에서 제공하도록 합니다.

개발 서버를 쉽게 실행할 수 있는 스크립트를 추가해보겠습니다.

package.json

 {
   "name": "webpack-demo",
   "version": "1.0.0",
   "description": "",
   "private": true,
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "watch": "webpack --watch",
+    "start": "webpack serve --open",
     "build": "webpack"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "devDependencies": {
     "html-webpack-plugin": "^5.6.6",
     "webpack": "^5.105.0",
     "webpack-cli": "^7.0.0",
     "webpack-dev-server": "^5.2.3"
   },
   "dependencies": {
     "lodash": "^4.17.21"
   }
 }

이제 커멘드 라인에서 npm start를 실행할 수 있으며 브라우저가 자동으로 페이지를 로드하는 것을 볼 수 있습니다. 이제 소스 파일을 변경하고 저장하면, 코드가 컴파일된 후 웹 서버가 자동으로 다시 로드됩니다. 시도해 보세요!

webpack-dev-server에는 설정 가능한 많은 옵션이 있습니다. 자세한 내용은 문서를 참고하세요.

Using webpack-dev-middleware

webpack-dev-middleware는 webpack에서 처리한 파일을 서버로 내보내는 래퍼 입니다. 이것은 내부적으로 webpack-dev-server에서 사용되지만, 사용자가 원하는 경우 더 많은 설정을 허용하기 위해 별도의 패키지로 사용할 수 있습니다. webpack-dev-middleware와 express 서버를 결합한 예를 살펴보겠습니다.

시작하기 전에 expresswebpack-dev-middleware를 설치하겠습니다.

npm install --save-dev express webpack-dev-middleware

이제 미들웨어가 올바르게 작동하는지 확인하기 위해 webpack의 설정 파일을 약간 수정해야 합니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   mode: 'development',
   entry: {
     index: './src/index.js',
     print: './src/print.js',
   },
   devtool: 'inline-source-map',
   devServer: {
     static: './dist',
   },
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Development',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
+    publicPath: '/',
   },
 };

http://localhost:3000에서 파일이 올바르게 제공되는지 확인하기 위해 publicPath가 서버 스크립트 내에서도 사용됩니다. 나중에 포트 번호를 지정합니다. 다음 단계는 커스텀 express 서버를 설정하는 것입니다.

project

  webpack-demo
   ├── package.json
   ├── package-lock.json
   ├── webpack.config.js
+  ├── server.js
   ├── /dist
   ├── /src
   │   ├── index.js
   │   └── print.js
   └── /node_modules

server.js

import express from "express";
import webpack from "webpack";
import webpackDevMiddleware from "webpack-dev-middleware";
import config from "./webpack.config.js";

const app = express();
const compiler = webpack(config);

// express에서 webpack-dev-middleware와 webpack.config.js를 사용하도록 설정하세요.
// 기본 설정 파일
app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath,
  }),
);

// 트 3000에서 파일 제공
app.listen(3000, () => {
  console.log("Example app listening on port 3000!\n");
});

이제 서버를 좀 더 쉽게 실행할 수 있도록 npm 스크립트를 추가합니다.

package.json

 {
   "name": "webpack-demo",
   "version": "1.0.0",
   "description": "",
   "private": true,
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "watch": "webpack --watch",
     "start": "webpack serve --open",
+    "server": "node server.js",
     "build": "webpack"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "devDependencies": {
     "express": "^5.2.1",
     "html-webpack-plugin": "^5.6.6",
     "webpack": "^5.105.0",
     "webpack-cli": "^7.0.0",
     "webpack-dev-middleware": "^8.0.3",
     "webpack-dev-server": "^5.2.3"
   },
   "dependencies": {
     "lodash": "^4.17.21"
   }
 }

이제 터미널에서 npm run server를 실행하면, 다음과 유사한 출력이 표시됩니다.

Example app listening on port 3000!
...
<i> [webpack-dev-middleware] asset index.bundle.js 1.38 MiB [emitted] (name: index)
<i> asset print.bundle.js 6.25 KiB [emitted] (name: print)
<i> asset index.html 274 bytes [emitted]
<i> runtime modules 1.9 KiB 9 modules
<i> cacheable modules 530 KiB
<i>   ./src/index.js 406 bytes [built] [code generated]
<i>   ./src/print.js 83 bytes [built] [code generated]
<i>   ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
<i> webpack 5.x.x compiled successfully in 709 ms
<i> [webpack-dev-middleware] Compiled successfully.
<i> [webpack-dev-middleware] Compiling...
<i> [webpack-dev-middleware] assets by status 1.38 MiB [cached] 2 assets
<i> cached modules 530 KiB (javascript) 1.9 KiB (runtime) [cached] 12 modules
<i> webpack 5.x.x compiled successfully in 19 ms
<i> [webpack-dev-middleware] Compiled successfully.

이제 브라우저를 실행하고 http://localhost:3000로 이동합니다. webpack 앱이 실행하고 작동하는 것을 확인할 수 있습니다!

Adjusting Your Text Editor

코드 자동 컴파일을 사용하면, 파일을 저장할 때 문제가 발생할 수 있습니다. 일부 편집기에는 잠재적으로 재컴파일을 방해할 수 있는 "안전한 쓰기" 기능이 있습니다.

일부 일반 편집기에서 이 기능을 비활성화하려면, 아래 목록을 참고하십시오.

  • Sublime Text 3: 사용자 환경 설정에 atomic_save: 'false'를 추가하십시오.
  • JetBrains IDEs (e.g. WebStorm): Preferences > Appearance & Behavior > System Settings에서 "Use safe write" 선택을 해제하십시오.
  • Vim: 설정에 :set backupcopy=yes를 추가하십시오.

Conclusion

이제 자동으로 코드를 컴파일하고 간단한 개발 서버를 실행하는 방법을 배웠으므로, 코드 스플리팅을 다룰 다음 가이드로 넘어가 볼까요?

Code Splitting

코드 스플리팅은 webpack의 가장 매력적인 기능 중 하나입니다. 이 기능을 사용하여 코드를 다양한 번들로 분할하고, 요청에 따라 로드하거나 병렬로 로드할 수 있습니다. 더 작은 번들을 만들고 리소스 우선순위를 올바르게 제어하기 위해서 사용하며, 잘 활용하면 로드 시간에 큰 영향을 끼칠 수 있습니다.

일반적으로 코드 스플리팅은 세 가지 방식으로 접근할 수 있습니다.

  • Entry Points: entry 설정을 사용하여 코드를 수동으로 분할합니다.
  • Prevent Duplication: Entry dependencies 또는 SplitChunksPlugin을 사용하여 중복 청크를 제거하고 청크를 분할합니다.
  • Dynamic Imports: 모듈 내에서 인라인 함수 호출을 통해 코드를 분할합니다.

Entry Points

코드를 분할하는 가장 쉽고 직관적인 방법입니다. 그러나 다른 방법에 비해 수동적이며, 같이 살펴볼 몇 가지 함정이 있습니다. 메인 번들에서 다른 모듈을 어떻게 분리하는지 알아보겠습니다.

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
+ │   └── another-module.js
  └── /node_modules

another-module.js

import _ from "lodash";

console.log(_.join(["Another", "module", "loaded!"], " "));

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
-  entry: './src/index.js',
+  mode: 'development',
+  entry: {
+    index: './src/index.js',
+    another: './src/another-module.js',
+  },
   output: {
-    filename: 'main.js',
+    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

다음과 같은 빌드 결과가 생성됩니다.

...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 245 ms

언급했듯이 이 접근 방식에는 몇 가지 함정이 있습니다.

  • 엔트리 청크 사이에 중복된 모듈이 있는 경우 두 번들에 모두 포함됩니다.
  • 코어 애플리케이션 로직을 통한 코드의 동적 분할에는 사용할 수 없으며 유연하지 않습니다.

이 중 첫 번째 항목을 통해 지금 예제의 문제를 알 수 있습니다. 왜냐하면 ./src/index.js에서도 lodash를 가져오므로 양쪽 번들에서 중복으로 포함되기 때문입니다. 다음 섹션에서 중복된 것을 제거하겠습니다.

Prevent Duplication

Entry dependencies

dependOn 옵션을 사용하면 청크간 모듈을 공유할 수 있습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   mode: 'development',
   entry: {
-    index: './src/index.js',
-    another: './src/another-module.js',
+    index: {
+      import: './src/index.js',
+      dependOn: 'shared',
+    },
+    another: {
+      import: './src/another-module.js',
+      dependOn: 'shared',
+    },
+    shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

단일 HTML 페이지에서 여러 엔트리 포인트를 사용하는 경우 optimization.runtimeChunk: 'single'도 필요합니다. 그렇지 않으면 여기에서 설명하는 문제가 발생할 수 있습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   mode: 'development',
   entry: {
     index: {
       import: './src/index.js',
       dependOn: 'shared',
     },
     another: {
       import: './src/another-module.js',
       dependOn: 'shared',
     },
     shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  optimization: {
+    runtimeChunk: 'single',
+  },
 };

다음은 빌드 결과입니다.

...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./src/index.js 257 bytes [built] [code generated]
webpack 5.x.x compiled successfully in 249 ms

보시다시피 shared.bundle.js, index.bundle.jsanother.bundle.js 외에 또 다른 runtime.bundle.js 파일이 생성됩니다.

webpack은 하나의 페이지에 여러 엔트리 포인트를 허용하지만, 가능하다면 entry: { page: ['./analytics', './app'] }처럼 여러 개의 import가 포함된 엔트리 포인트 사용을 피해야 합니다. 이는 async 스크립트 태그를 사용할 때 최적화에 용이하며 일관된 순서로 실행할 수 있도록 합니다.

SplitChunksPlugin

SplitChunksPlugin을 사용하면 기존 엔트리 청크 또는 완전히 새로운 청크로 공통 의존성을 추출할 수 있습니다. 이를 활용하여 이전 예제의 lodash 중복을 제거해 보겠습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all',
+     },
+   },
  };

optimization.splitChunks 설정 옵션을 적용하면 index.bundle.jsanother.bundle.js에서 중복 의존성이 제거된 것을 확인 할 수 있습니다. 플러그인은 lodash를 별도의 청크로 분리하고 메인 번들에서도 제거된 것을 알 수 있습니다. 그러나 공통 의존성은 webpack에서 지정한 크기 임계값을 충족하는 경우에만 별도의 청크로 추출된다는 점에 유의해야 합니다.

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 241 ms

다음은 코드 스플리팅을 위해 커뮤니티에서 제공하는 다른 유용한 플러그인과 로더입니다.

-mini-css-extract-plugin : 메인 애플리케이션에서 CSS를 분리하는데 유용합니다.

Dynamic Imports

webpack은 동적 코드 스플리팅에 두 가지 유사한 기술을 지원합니다. 첫 번째이자 권장하는 접근 방식은 ECMAScript 제안을 준수하는 import()구문을 사용하는 방식입니다. 기존의 webpack 전용 방식은 require.ensure를 사용하는 것입니다. 이 두 가지 중 첫 번째를 사용해 보겠습니다.

시작하기 전에 위 예제의 설정에서 추가 entryoptimization.splitChunks를 제거하겠습니다. 다음 데모에는 필요하지 않습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   mode: 'development',
   entry: {
     index: './src/index.js',
-    another: './src/another-module.js',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
-  optimization: {
-    splitChunks: {
-      chunks: 'all',
-    },
-  },
 };

또한 현재 사용하지 않는 파일을 프로젝트에서 제거하겠습니다.

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
- │   └── another-module.js
  └── /node_modules

이제 정적으로 가져오던 lodash를 동적으로 가져와서 청크를 분리해보겠습니다.

src/index.js

-import _ from 'lodash';
-
-function component() {
+function getComponent() {
-  const element = document.createElement('div');

-  // 이제 이 스크립트가 Lodash를 import합니다.
-  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  return import('lodash')
+    .then(({ default: _ }) => {
+      const element = document.createElement('div');
+
+      element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-  return element;
+      return element;
+    })
+    .catch((error) => 'An error occurred while loading the component');
 }

-document.body.appendChild(component());
+getComponent().then((component) => {
+  document.body.appendChild(component);
+});

default가 필요한 이유는 webpack 4 이후로 CommonJS 모듈을 가져올 때 더 이상 module.exports 값 으로 해석되지 않으며 대신 CommonJS 모듈에 대한 인공 네임 스페이스 객체를 생성하기 때문입니다. 그 이유에 대한 자세한 내용은 webpack 4: import() 및 CommonJs를 참고하세요.

webpack을 실행하여 lodash가 별도의 번들로 분리되어 있는지 살펴보겠습니다.

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
  ./src/index.js 434 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 268 ms
import(
  /* webpackExports: ["default", "namedExport"] */
  "./module"
);

이렇게 하면 웹팩이 사용되지 않는 다른 내보내기를 트리 쉐이킹하는 데 도움이 될 수 있습니다. 자세한 내용은 매직 주석을 참고하세요.

import()는 프로미스를 반환하므로 async 함수와 함께 사용할 수 있습니다. 다음은 이를 사용하여 코드를 간소화하는 방법입니다.

src/index.js

-function getComponent() {
+async function getComponent() {
+  const element = document.createElement('div');
+  const { default: _ } = await import('lodash');

-  return import('lodash')
-    .then(({ default: _ }) => {
-      const element = document.createElement('div');
+  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-      element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-      return element;
-    })
-    .catch((error) => 'An error occurred while loading the component');
+  return element;
 }

 getComponent().then((component) => {
   document.body.appendChild(component);
 });

Understanding ChunkLoadError

동적 import() 또는 코드 분할을 사용할 때, Webpack은 런타임에 청크 로드에 실패하면 ChunkLoadError를 발생시킬 수 있습니다.

이 오류는 일반적으로 요청된 청크가 제대로 실행되거나 해결되지 못했음을 나타냅니다. 경우에 따라 브라우저의 기본 네트워크 오류 또는 스크립트 로딩 오류가 ChunkLoadError 메시지 자체에 완전히 반영되지 않을 수도 있습니다.

다음과 같은 오류가 발생하는 경우:

  • 청크 파일이 네트워크를 통해 접근 가능한지 확인합니다.
  • publicPath가 올바르게 구성되었는지 확인합니다.
  • 브라우저 콘솔에서 추가적인 스크립트 또는 네트워크 오류가 있는지 확인합니다.

더 자세한 내용은 webpack 이슈 트래커의 관련 논의를 참조하세요.

Prefetching/Preloading modules

Webpack 4.6.0+에서 프리페치 및 프리로드에 대한 지원이 추가되었습니다.

모듈을 가져올 때 인라인 지시문을 사용하면 webpack이 브라우저에 아래와 같은 "리소스 힌트"를 줄 수 있습니다.

  • prefetch : 향후 일부 탐색에 리소스가 필요할 수 있습니다.
  • preload : 현재 탐색 중에 리소스도 필요합니다.

간단한 프리페치의 예제를 들어보겠습니다. HomePage 컴포넌트에서 LoginButton 컴포넌트를 렌더링하고, 이 컴포넌트를 클릭하면 LoginModal 컴포넌트를 요청하여 로드하는 경우입니다.

LoginButton.js

// ...
import(/* webpackPrefetch: true */ "./path/to/LoginModal.js");

이는 페이지 head에 <link rel="prefetch" href="login-modal-chunk.js">를 추가하고 브라우저에 login-modal-chunk.js를 유휴 시간에 미리 가져오도록 지시합니다.

프리로드 지시문은 프리페치와 비교했을 때 여러 가지 차이점이 있습니다.

  • 프리로드 청크는 부모 청크와 병렬로 로드를 시작합니다. 프리페치 청크는 부모 청크가 로드 완료된 후에 로드를 시작합니다.
  • 프리로드 청크는 중간 우선순위를 가지며 즉시 다운로드됩니다. 프리페치 청크는 브라우저가 유휴 상태일 때 다운로드 됩니다.
  • 프리로드 청크는 부모 청크에서 즉시 요청 되어야 합니다. 프리페치 청크는 나중에 언제라도 사용할 수 있습니다.
  • 지원하는 브라우저에 차이가 있습니다.

간단한 프리로드의 예로는, 별도의 청크에 있어야 하는 큰 라이브러리에 항상 의존하는 Component를 생각해 볼 수 있습니다.

거대한 ChartingLibrary가 필요한 ChartComponent를 상상해 봅시다. 렌더링 될 때 LoadingIndicator를 표시하고 즉시 ChartingLibrary를 요청하여 불러옵니다.

ChartComponent.js

// ...
import(/* webpackPreload: true */ "ChartingLibrary");

ChartComponent를 사용하는 페이지를 요청할 때 <link rel="preload">를 통해 charting-library-chunk도 요청됩니다. page-chunk가 더 작고 더 빨리 완료된다고 가정하면 이미 요청된 charting-library-chunk가 완료될 때까지 페이지에는 LoadingIndicator가 표시됩니다. 두 번이 아닌 한 번의 라운드 트립이 필요하므로 대기 시간이 긴 환경에서 로드 시간이 증가할 수 있습니다.

때로는 프리로드에 대한 자신만의 제어가 필요합니다. 예를 들어 모든 동적 import의 프리로드는 비동기 스크립트를 통해 수행할 수 있습니다. 이는 서버사이드 랜더링을 스트리밍할 때 유용합니다.

const lazyComp = () =>
  import("DynamicComponent").catch((error) => {
    // 에러가 있는 작업을 수행합니다.
    // 예를 들어, 모든 네트워크 에러가 발생할 경우 요청을 재시도할 수 있습니다.
  });

Webpack이 해당 스크립트의 자체 로드를 시작하기 전에 스크립트 로드가 실패하면(Webpack은 해당 스크립트가 페이지에 없는 경우 해당 코드를 로드하기 위해 스크립트 태그를 생성함), 해당 catch 핸들러는 chunkLoadTimeout에 전달되지 않습니다. 이 동작은 예기치 않은 것일 수 있습니다. 하지만 설명 가능합니다. Webpack은 해당 스크립트가 실패했다는 것을 모르기 때문에 에러를 발생시킬 수 없습니다. Webpack은 에러가 발생한 후 즉시 onerror 핸들러를 스크립트에 추가합니다.

이러한 문제를 방지하기 위해, 에러 발생 시 스크립트를 제거하는 자체 onerror 핸들러를 추가할 수 있습니다.

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

이 경우 에러가 있는 스크립트는 제거됩니다. Webpack은 자체 스크립트를 생성하고 모든 에러는 시간 초과 없이 처리됩니다.

Bundle Analysis

코드 스플리팅을 시작하면 출력을 분석하여 어디서 모듈이 종료되었는지 확인하는 데 유용합니다. 공식 분석 도구부터 시작하는 것이 좋습니다. 커뮤니티에서 지원하는 다른 옵션도 있습니다.

  • webpack-chart: webpack 통계를 위한 인터렉티브 원형 차트.
  • webpack-visualizer: 번들을 시각화하고 분석하여 어떤 모듈이 공간을 차지하고 있고 어떤 모듈이 중복될 수 있는지 확인합니다.
  • webpack-bundle-analyzer: 확대/축소 가능한 편리한 인터렉티브 트리 맵으로 번들 콘텐츠를 표현하는 플러그인 및 CLI 유틸리티입니다.
  • webpack bundle optimize helper: 이 도구는 번들을 분석하고 번들 크기를 줄이기 위한 실용적인 개선 사항을 제공합니다.
  • bundle-stats: 번들 보고서(번들 크기, 애셋, 모듈)를 생성하고 서로 다른 빌드 간의 결과를 비교합니다.
  • webpack-stats-viewer: Webpack 통계용 빌드가 포함된 플러그인입니다. Webpack 번들 세부에 대한 자세한 정보를 표시합니다.

Next Steps

실제 애플리케이션에서 어떻게 import()를 사용하는지 더 구체적으로 알고 싶다면 Lazy Loading의 예제를 확인하세요. 더 효율적인 코드 스플리팅 방법은 Caching을 참고하세요.

Caching

우리는 배포 가능한 /dist디렉터리를 생성하는 모듈형 애플리케이션을 번들링하기 위해 webpack을 사용하고 있습니다. 일단 서버에 /dist의 콘텐츠가 배포되면 클라이언트(일반적으로 브라우저)가 해당 서버에 접근하여 사이트와 애셋을 가져옵니다. 마지막 단계는 시간이 많이 걸릴 수 있기 때문에 브라우저는 캐싱이라는 기술을 사용합니다. 이렇게 하면 불필요한 네트워크 트래픽을 줄이면서 사이트를 더 빨리 로드할 수 있습니다. 그러나 새 코드를 불러올 경우에는 어려움을 느낄 수 있습니다.

이 가이드는 webpack 컴파일로 생성 된 파일의 내용이 변경되지 않는 한 캐시된 상태로 유지되도록 하는 데 필요한 설정에 초점을 맞춥니다.

Output Filenames

output.filename substitutions 설정을 사용하여 출력 파일의 이름을 정의할 수 있습니다. Webpack은 substitutions 이라고 하는 대괄호 문자열을 사용하여 파일 이름을 템플릿화하는 방법을 제공합니다. [contenthash] substitution은 애셋의 콘텐츠에 따라 고유한 해시를 추가합니다. 애셋의 콘텐츠가 변경되면 [contenthash]도 변경됩니다.

index.html 파일을 수동으로 관리할 필요가 없도록 출력 관리의 플러그인시작하기의 예제를 사용하여 프로젝트를 설정해 보겠습니다.

project

webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 ├── /src
 │   └── index.js
 └── /node_modules

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   plugins: [
     new HtmlWebpackPlugin({
-      title: 'Output Management',
+      title: 'Caching',
     }),
   ],
   output: {
-    filename: 'bundle.js',
+    filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
 };

이 설정으로 빌드 스크립트 npm run build를 실행하면, 다음과 같은 출력이 생성됩니다.

...
                       Asset       Size  Chunks                    Chunk Names
main.7e2c49a622975ebd9b7e.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]
...

보다시피, 번들의 이름은 해시를 통해 콘텐츠를 반영합니다. 변경하지 않고 다른 빌드를 실행하면 해당 파일 이름이 동일하게 유지될 거라고 생각합니다. 그러나 다시 실행하면 이 경우에는 그렇지 않을 수 있다는 것을 알 수 있습니다.

...
                       Asset       Size  Chunks                    Chunk Names
main.205199ab45963f6a62ec.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]
...

이것은 webpack이 특정 보일러플레이트, 특히 런타임과 매니페스트를 엔트리 청크에 포함하기 때문입니다.

Extracting Boilerplate

코드 스플릿팅에서 배운 것처럼 SplitChunksPlugin을 사용하여 모듈을 별도의 번들로 분할 할 수 있습니다. Webpack은 optimization.runtimeChunk 옵션을 사용하여 런타임 코드를 별도의 청크로 분할하는 최적화 기능을 제공합니다. 모든 청크에 대해 단일 런타임 번들을 생성하려면 single로 설정합니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Caching',
     }),
   ],
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
+  optimization: {
+    runtimeChunk: 'single',
+  },
 };

추출한 런타임 번들을 보기위해 다른 빌드를 실행해 보겠습니다.

Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
   main.e81de2cf758ada72f306.js   69.5 KiB       1  [emitted]  main
                     index.html  275 bytes          [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
    + 1 hidden module

lodash 또는 react와 같은 타사 라이브러리는 로컬 소스 코드보다 변경 될 가능성이 적기 때문에 별도의 vendor 청크로 추출하는 것도 좋은 방법입니다. 이 단계를 통해 클라이언트는 최신 상태를 유지하기 위해 서버에 더 적은 요청을 할 수 있습니다. 이는 Example 2 of SplitChunksPlugin에 표시된 SplitChunksPlugincacheGroups 옵션을 사용하여 수행할 수 있습니다. cacheGroups과 함께 optimization.splitChunks를 추가하고 다음 파라미터를 사용하여 빌드합니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Caching',
     }),
   ],
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
   optimization: {
     runtimeChunk: 'single',
+    splitChunks: {
+      cacheGroups: {
+        vendor: {
+          test: /[\\/]node_modules[\\/]/,
+          name: 'vendors',
+          chunks: 'all',
+        },
+      },
+    },
   },
 };

새로운 vendor번들을 확인하기 위해 다른 빌드를 실행해 보겠습니다.

...
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
vendors.a42c3ca0d742766d7a28.js   69.4 KiB       1  [emitted]  vendors
   main.abf44fedb7d11d4312d7.js  240 bytes       2  [emitted]  main
                     index.html  353 bytes          [emitted]
...

이제 main 번들에 node_modules 디렉터리의 vendor 코드가 포함되어 있지 않고 크기가 240 bytes로 줄어든 것을 볼 수 있습니다!

Module Identifiers

프로젝트에 다른 모듈print.js를 추가해 보겠습니다.

프로젝트

webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 ├── /src
 │   ├── index.js
+│   └── print.js
 └── /node_modules

print.js

+ export default function print(text) {
+   console.log(text);
+ };

src/index.js

  import _ from 'lodash';
+ import Print from './print';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

다른 빌드를 실행하면 main 번들의 해시만 변경 될 것으로 예상합니다, 하지만...

...
                           Asset       Size  Chunks                    Chunk Names
  runtime.1400d5af64fc1b7b3a45.js    5.85 kB      0  [emitted]         runtime
  vendor.a7561fb0e9a071baadb9.js     541 kB       1  [emitted]  [big]  vendor
    main.b746e3eb72875af2caa9.js    1.22 kB       2  [emitted]         main
                      index.html  352 bytes          [emitted]
...

... 세가지 모두가 변경 된 것을 볼 수 있습니다. 이는 각 module.id가 기본적으로 해석 순서에 따라 증가하기 때문입니다. 해석 순서가 변경되면 ID도 변경됩니다. 그래서 요약하자면:

  • 새로운 콘텐츠로 인해 main 번들이 변경되었습니다.
  • module.id가 바뀌어 vendor 번들이 변경되었습니다.
  • 그리고, runtime 번들은 이제 새로운 모듈에 대한 참조를 포함하기 때문에 변경되었습니다.

첫 번째와 마지막은 우리가 고치고 싶은 vendor 해시입니다. 'deterministic'옵션과 함께 optimization.moduleIds를 사용하겠습니다.

webpack.config.js

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import HtmlWebpackPlugin from 'html-webpack-plugin';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Caching',
     }),
   ],
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
   optimization: {
+    moduleIds: 'deterministic',
     runtimeChunk: 'single',
     splitChunks: {
       cacheGroups: {
         vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'vendors',
           chunks: 'all',
         },
       },
     },
   },
 };

이제 새로운 로컬 의존성에도 불구하고 vendor해시는 빌드간에 일관성을 유지해야합니다.

...
                          Asset       Size  Chunks             Chunk Names
   main.216e852f60c8829c2289.js  340 bytes       0  [emitted]  main
vendors.55e79e5927a639d21a1b.js   69.5 KiB       1  [emitted]  vendors
runtime.725a1a51ede5ae0cfde0.js   1.42 KiB       2  [emitted]  runtime
                     index.html  353 bytes          [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...

그리고 src/index.js를 수정하여 추가 의존성을 일시적으로 제거해 보겠습니다.

src/index.js

  import _ from 'lodash';
- import Print from './print';
+ // import Print from './print';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-   element.onclick = Print.bind(null, 'Hello webpack!');
+   // element.onclick = Print.bind(null, 'Hello webpack!');

    return element;
  }

  document.body.appendChild(component());

마지막으로 빌드를 다시 실행합니다.

...
                          Asset       Size  Chunks             Chunk Names
   main.ad717f2466ce655fff5c.js  274 bytes       0  [emitted]  main
vendors.55e79e5927a639d21a1b.js   69.5 KiB       1  [emitted]  vendors
runtime.725a1a51ede5ae0cfde0.js   1.42 KiB       2  [emitted]  runtime
                     index.html  353 bytes          [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...

두 빌드 모두 55e79e5927a639d21a1b를 vendor 번들 파일 이름으로 표시한 것을 알 수 있습니다.

Conclusion

캐싱은 복잡할 수 있지만, 애플리케이션이나 사이트 사용자에게 주는 이점으로 그만한 가치가 있습니다. 자세한 내용은 아래의 추가 자료 섹션을 참고하세요.

Authoring Libraries

애플리케이션 외에도 JavaScript 라이브러리를 번들링 할 때도 webpack을 사용할 수 있습니다. 아래의 가이드는 번들링 전략을 간소화하려는 라이브러리 작성자를 위한 것입니다.

Authoring a Library

사용자가 1부터 5까지의 숫자를 숫자 표현에서 텍스트로 또는 그 반대로 변환할 수 있는 작은 라이브러리 webpack-numbers를 작성한다고 가정해 보겠습니다. 예. 2 에서 'two'.

프로젝트의 기본 구조는 다음과 같을 것입니다.

project

+ ├── webpack.config.js
+ ├── package.json
+ └── /src
+     ├── index.js
+     └── ref.json

npm으로 프로젝트를 초기화한 다음, webpack, webpack-cli, lodash를 개발 의존성으로 설치합니다.

npm init -y
npm install --save-dev webpack webpack-cli lodash

처음에는 lodash를 라이브러리에 함께 번들할 것이므로 devDependency로 설치합니다. 최종 출력물에 포함되기 때문에 라이브러리 사용자는 이를 별도로 설치할 필요가 없습니다.

src/ref.json

[
  {
    "num": 1,
    "word": "One"
  },
  {
    "num": 2,
    "word": "Two"
  },
  {
    "num": 3,
    "word": "Three"
  },
  {
    "num": 4,
    "word": "Four"
  },
  {
    "num": 5,
    "word": "Five"
  },
  {
    "num": 0,
    "word": "Zero"
  }
]

src/index.js

import _ from "lodash";
import numRef from "./ref.json";

export function numToWord(num) {
  return _.reduce(
    numRef,
    (accum, ref) => (ref.num === num ? ref.word : accum),
    "",
  );
}

export function wordToNum(word) {
  return _.reduce(
    numRef,
    (accum, ref) => (ref.word === word && word.toLowerCase() ? ref.num : accum),
    -1,
  );
}

Webpack Configuration

아래의 기본적인 webpack 설정으로 시작해봅시다.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "webpack-numbers.js",
  },
};

webpack으로 애플리케이션을 번들해보았다면 익숙할 것입니다. 기본적으로 webpack에게 src/index.jsdist/webpack-numbers.js로 번들하도록 지시합니다.

Adding Source Maps

When bundling a library, it is recommended to generate source maps. Source maps allow consumers of your library to debug your original source code rather than the minified bundle. This can be done using the devtool option:

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: './src/index.js',
+   devtool: 'source-map',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js',
    },
  };

Expose the Library

지금까지는 애플리케이션 번들링과 동일하며 다른 점은 output.library 옵션을 통해 엔트리 포인트를 export 해야 합니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js',
+     library: 'webpackNumbers',
    },
  };

사용자가 script 태그를 통해 사용할 수 있도록 엔트리 포인트를 webpackNumbers로 export 했습니다.

<script src="https://example.org/webpack-numbers.js"></script>
<script>
  window.webpackNumbers.wordToNum("Five");
</script>

그러나, script 태그를 통해 참조될 때만 작동하며 CommonJS, AMD, Node.js 등과 같은 다른 환경에서는 사용할 수 없습니다.

라이브러리 작성자는 다양한 환경에서 호환되기를 원합니다. 즉, 사용자가 아래 나열된 여러 방법으로 번들 된 라이브러리를 사용할 수 있어야 합니다.

  • CommonJS module require:

    const webpackNumbers = require("webpack-numbers");
    
    // ...
    webpackNumbers.wordToNum("Two");
  • AMD module require:

    require(["webpackNumbers"], (webpackNumbers) => {
      // ...
      webpackNumbers.wordToNum("Two");
    });
  • script tag:

    <!DOCTYPE html>
    <html>
      ...
      <script src="https://example.org/webpack-numbers.js"></script>
      <script>
        // ...
        // 전역 변수
        webpackNumbers.wordToNum("Five");
        // window 객체의 프로퍼티
        window.webpackNumbers.wordToNum("Five");
        // ...
      </script>
    </html>

type'umd'로 설정하여 output.library 옵션을 업데이트해 보겠습니다.

 import path from 'node:path';
 import { fileURLToPath } from 'node:url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     path: path.resolve(__dirname, 'dist'),
     filename: 'webpack-numbers.js',
-    library: 'webpackNumbers',
+    globalObject: 'this',
+    library: {
+      name: 'webpackNumbers',
+      type: 'umd',
+    },
   },
 };

webpack은 라이브러리를 CommonJS, AMD, script 태그에서 사용할 수 있도록 번들할 것입니다.

Externalize Lodash

이제 npx webpack을 실행하면 다소 큰 번들이 생성되는 것을 볼 수 있습니다. 파일을 살펴보면 lodash가 코드와 함께 번들되어 있습니다. lodash를 번들에 포함해 라이브러리가 비대해지는 것을 막기 위해 webpack이 이를 외부 모듈로 취급하도록 설정할 수 있습니다. 더 이상 번들하지 않으므로 사용자가 이를 직접 제공해야 합니다. 따라서 라이브러리 사용자의 패키지 매니저가 자동으로 설치할 수 있도록 lodashdevDependencies에서 dependencies 또는 peerDependencies로 옮겨야 합니다.

externals 설정을 사용하면 됩니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js',
      library: {
        name: 'webpackNumbers',
        type: 'umd',
      },
    },
+   externals: {
+     lodash: {
+       commonjs: 'lodash',
+       commonjs2: 'lodash',
+       amd: 'lodash',
+       root: '_',
+     },
+   },
  };

이는 라이브러리가 사용자 환경에서 lodash라는 종속성을 사용할 수 있다고 예상한다는 것을 의미합니다.

External Limitations

종속성에서 여러 파일을 사용하는 라이브러리의 경우:

import A from "library/one";
import B from "library/two";

// ...

externals에서 library를 지정하여 번들에서 제외할 수 없습니다. 하나씩 또는 정규식을 사용하여 제외해야 합니다.

export default {
  // ...
  externals: [
    "library/one",
    "library/two",
    // "library/"로 시작하는 모든 것
    /^library\/.+$/,
  ],
};

Final Steps

프로덕션 가이드에 언급된 단계에 따라 프로덕션에 맞게 출력을 최적화하세요. 또한 생성된 번들의 경로를 package.jsonmain 필드에 추가하세요.

package.json

{
  ...
  "main": "dist/webpack-numbers.js",
  ...
}

또는 이 가이드에 따라 표준 모듈로 추가하세요.

{
  ...
  "module": "src/index.js",
  ...
}

mainpackage.json의 표준을, module은 JavaScript 생태계 업그레이드가 하위 호환성을 깨지 않고 ES2015 모듈을 사용할 수 있도록 하는 제안[1] [2]을 의미합니다.

이제 사용자에게 배포하기 위해 npm 패키지로 게시하고 unpkg.com에서 찾을 수 있습니다.

Environment Variables

webpack.config.js에서 development와 production의 빌드를 명확하게 구분하기 위해 환경 변수를 사용할 수 있습니다.

webpack 커맨드라인 환경 옵션인 --env 를 사용하면 원하는 만큼 많은 환경 변수를 전달할 수 있습니다. 환경 변수는 webpack.config.js에서 액세스 할 수 있습니다. 예를 들면, --env production--env goal=local.

npx webpack --env goal=local --env production --progress

Webpack 설정에서 한 가지 변경 사항을 적용해야 합니다. 일반적으로 export default는 설정 객체를 가리킵니다. env 변수를 사용하려면 export default를 함수로 변환해야 합니다.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default (env) => {
  // 여기에서 env.<변수> 를 사용하세요.
  console.log("Goal:", env.goal); // 'local'
  console.log("Production:", env.production); // true

  return {
    entry: "./src/index.js",
    output: {
      filename: "bundle.js",
      path: path.resolve(__dirname, "dist"),
    },
  };
};

Build Performance

이 가이드에는 빌드/컴파일 성능을 개선하기 위한 몇 가지 유용한 팁이 포함되어 있습니다.


General

다음의 모범 사례는 development 또는 production에서 빌드 스크립트를 실행하는 경우 도움이 될 것입니다.

Stay Up to Date

최신 webpack 버전을 사용하세요. 우리는 항상 성능을 개선하고 있습니다. webpack의 권장 최신 버전은 다음과 같습니다.

latest webpack version

Node.js를 최신 상태로 유지하면 성능에 도움이 될 수 있습니다. 또한 패키지 관리자(예: npm 또는 yarn)를 최신 상태로 유지하는 것도 도움이 될 수 있습니다. 최신 버전은 더 효율적인 모듈 트리를 생성하고 해석하는 속도를 높입니다.

Loaders

최소한으로 필요한 모듈에만 로더를 적용하세요.

export default {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: "babel-loader",
      },
    ],
  },
};

위와 같은 방식보다는 아래처럼 include 필드를 사용하여 실제로 변환해야 하는 모듈에만 로더를 적용합니다.

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "src"),
        loader: "babel-loader",
      },
    ],
  },
};

Bootstrap

각각의 추가 로더/플러그인에는 부팅 시간이 있습니다. 가능한 한 도구를 적게 사용하세요.

Resolving

아래의 단계들로 해석 속도를 향상 시킬 수 있습니다.

  • 파일 시스템의 호출 수가 증가되기 때문에 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles의 항목 수를 최소화하세요.
  • 심볼릭 링크를 사용하지 않는 경우 resolve.symlinks: false를 설정하세요(예: npm link 또는 yarn link).
  • 컨텍스트에 특정적이지 않은 커스텀 해석 플러그인을 사용하는 경우 resolve.cacheWithContext: false를 설정하세요.

Dlls

자주 변경되지 않는 코드를 별도의 컴파일로 이동하려면 DllPlugin을 사용하세요. 이렇게 하면 빌드 프로세스가 복잡해 지지만 애플리케이션의 컴파일 속도가 향상됩니다.

Smaller = Faster

빌드 성능을 높이려면 컴파일의 총 크기를 줄이세요. 청크를 작게 유지하세요.

  • 더 적고 작은 라이브러리 사용
  • 다중 페이지 애플리케이션에서 SplitChunksPlugin을 사용
  • 다중 페이지 애플리케이션의 async 모드에서 SplitChunksPlugin을 사용
  • 사용하지 않는 코드를 제거
  • 현재 개발중인 코드의 일부만 컴파일

Worker Pool

thread-loader는 작업량이 큰 로더를 worker 풀에 작업을 분담할 때 사용할 수 있습니다.

Persistent cache

webpack 설정에서 cache 옵션을 사용하세요. package.json"postinstall"에서 캐시 디렉터리를 지우세요.

Custom plugins/loaders

커스텀 플러그인과 로더에서 성능 문제가 발생하지 않도록 프로파일 하세요.

Progress plugin

webpack 구성에서 ProgressPlugin을 제거하여 빌드 시간을 단축 할 수 있습니다. ProgressPlugin은 빠른 빌드에 유용하지 않을 수 있기 때문에 이점을 잘 활용하고 있는지 확인하세요.


Development

다음 단계는 개발 단계에서 특히 유용합니다.

Incremental Builds

webpack의 watch 모드를 사용하세요. 다른 도구를 사용하여 파일을 보고 webpack을 호출하지 마세요. 내장된 watch 모드는 타임 스탬프를 추적하고 캐시 무효화를 위해 이 정보를 컴파일에 전달합니다.

일부 설정에서는 watch가 폴링 모드로 돌아갑니다. watch 되는 파일이 많으면 이로 인해 많은 CPU 로드가 발생할 수 있습니다. 이 경우 watchOptions.poll을 사용하여 폴링 간격을 늘릴 수 있습니다.

Compile in Memory

아래의 유틸리티는 디스크에 쓰는 대신 메모리에서 애셋을 컴파일하고 제공하여 성능을 향상시킵니다.

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

stats.toJson speed

Webpack 4는 기본적으로 stats.toJson()을 사용하여 많은 양의 데이터를 출력합니다. 증분 단계에서 필요한 경우가 아니면 stats 개체의 일부를 찾지 마세요. v3.1.3 이후의 webpack-dev-server에는 증분 빌드 단계에서 stats 객체에서 검색되는 데이터의 양을 최소화하기 위한 상당한 성능 수정이 포함되었습니다.

Devtool

서로 다른 devtool 설정 간의 성능 차이에 유의하세요.

  • "eval"은 성능이 좋지만 트랜스파일 된 코드에는 도움이 되지 않습니다.
  • cheap-source-map 변형은 매핑의 질이 약간 떨어지지만, 성능이 좋습니다.
  • 증분 빌드에서는 eval-source-map 변형을 사용합니다.

Avoid Production Specific Tooling

특정 유틸리티, 플러그인, 로더는 production 빌드에서만 의미가 있습니다. 예를 들어 개발 중에 MinimizerPlugin으로 코드를 minify하고 mangle하는 것은 일반적으로 적절하지 않습니다. 이런 도구는 보통 개발 단계에서 제외해야 합니다.

  • MinimizerPlugin
  • [fullhash]/[chunkhash]/[contenthash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

Minimal Entry Chunk

Webpack은 파일 시스템에 업데이트된 청크만 내보냅니다. 일부 설정 옵션의 경우(HMR, output.chunkFilename,[fullhash] 안의 [name]/[chunkhash]/[contenthash]) 변경된 청크와 함께 엔트리 청크가 무효화됩니다.

엔트리 청크를 작게 유지하여 내보내는 비용이 저렴한지 확인하세요. 아래의 설정은 런타임 코드에 대한 추가 청크를 생성하므로 생성 비용이 저렴합니다.

export default {
  // ...
  optimization: {
    runtimeChunk: true,
  },
};

Avoid Extra Optimization Steps

Webpack은 크기 및 부하 성능에 대한 출력을 최적화하기 위해 추가 알고리즘 작업을 수행합니다. 이러한 최적화는 작은 코드 베이스에서는 성능이 좋지만 큰 코드에서는 비용이 많이들 수 있습니다.

export default {
  // ...
  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
  },
};

Output Without Path Info

Webpack은 출력 번들에 경로 정보를 생성하는 기능이 있습니다. 그러나 이것은 수천 개의 모듈을 번들로 묶는 프로젝트에서 가비지 컬렉션에 과부화를 줍니다. options.output.pathinfo 설정에서 이 기능을 끄세요.

export default {
  // ...
  output: {
    pathinfo: false,
  },
};

Node.js Versions 8.9.10-9.11.1

Node.js 버전 8.9.10 - 9.11.1의 ES2015 MapSet 구현에서 성능 저하가 있었습니다. Webpack은 이러한 데이터 구조를 자유롭게 사용하므로 이 성능저하는 컴파일 시간에 영향을 줍니다.

이전 및 이후 Node.js 버전은 영향을 받지 않습니다.

TypeScript Loader

ts-loader를 사용할 때 빌드 시간을 개선하려면 transpileOnly 로더 옵션을 사용하세요. 이 옵션은 자체적으로 타입 검사를 해제합니다. 타입 검사를 다시 받으려면 ForkTsCheckerWebpackPlugin을 사용하세요. 이렇게 각각 별도의 프로세스로 이동시키면 TypeScript 유형 검사 및 ESLint linting 속도가 빨라집니다.

export default {
  // ...
  test: /\.tsx?$/,
  use: [
    {
      loader: "ts-loader",
      options: {
        transpileOnly: true,
      },
    },
  ],
};

Production

다음 단계는 production에서 특히 유용합니다.

Source Maps

소스맵은 비용이 많이 듭니다. 정말로 필요한가요?


Specific Tooling Issues

다음 도구에는 빌드 성능을 저하시킬 수 있는 특정 문제가 있습니다.

Babel

  • preset/plugins 수를 최소화하세요.

TypeScript

  • 별도의 프로세스에서 타입 검사를 위해 fork-ts-checker-webpack-plugin을 사용하세요.
  • 타입 검사를 건너뛰도록 로더를 설정합니다.
  • happyPackMode: true / transpileOnly: true에서 ts-loader를 사용합니다.

Sass

  • node-sass에는 Node.js 스레드 풀의 스레드를 차단하는 버그가 있습니다. thread-loader와 함께 사용하는 경우 workerParallelJobs: 2를 설정하세요.

Content Security Policies

Webpack은 로드하는 모든 스크립트에 nonce를 추가할 수 있습니다. 기능 세트를 활성화하려면 엔트리 스크립트에 __webpack_nonce__ 변수를 포함해야 합니다. 고유한 해시 기반 nonce가 생성되고 고유한 페이지 뷰에 대해 각각 제공됩니다. 이것이 바로 __webpack_nonce__ 가 설정이 아닌 엔트리 파일에 지정된 이유입니다. __webpack_nonce__는 항상 base64로 인코딩된 문자열이어야 합니다.

Examples

엔트리 파일 안의 경우:

// ...
__webpack_nonce__ = "c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=";
// ...

Enabling CSP

CSP는 기본적으로 활성화되어 있지 않습니다. 브라우저에 CSP를 사용하도록 지시하려면 해당하는 헤더인 Content-Security-Policy 혹은 메타 태그 <meta http-equiv="Content-Security-Policy" ...>를 도큐먼트와 함께 보내야 합니다. 다음은 CDN 허용 리스트 URL을 포함한 CSP 헤더의 예시입니다.

Content-Security-Policy: default-src 'self'; script-src 'self'
https://trusted.cdn.com;

CSP 및 nonce 속성에 대한 자세한 내용은 이 페이지 하단의 더 읽어보기 섹션을 참고하세요.

Trusted Types

Webpack은 또한 Trusted Types을 사용하여 동적으로 구성된 스크립트를 로드하고 CSP require-trusted-types-for 지시문의 제한을 준수할 수 있습니다. output.trustedTypes 설정 옵션을 참고하세요.

Development - Vagrant

Vagrant를 사용하여 가상 머신에서 개발 환경을 실행하는 경우, 가상 머신에서도 webpack을 실행하고 싶을 수 있습니다.

Configuring the Project

시작하려면, Vagrantfile에 고정 IP가 있는지 확인하세요.

Vagrant.configure("2") do |config|
  config.vm.network :private_network, ip: "10.10.10.61"
end

다음으로, 프로젝트에 webpack, webpack-cli, @webpack-cli/serve, webpack-dev-server 를 설치하세요.

npm install --save-dev webpack webpack-cli @webpack-cli/serve webpack-dev-server

webpack.config.js 파일이 있는지 확인하세요. 만약 파일이 없다면 다음을 최소한의 예제로 사용해 시작하세요.

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  context: __dirname,
  entry: "./app.js",
};

그리고 index.html 파일을 만듭니다. 스크립트 태그는 번들을 가리켜야 합니다. output.filename이 지정되어 있지 않다면, bundle.js가 됩니다.

<!DOCTYPE html>
<html>
  <head>
    <script src="/bundle.js" charset="utf-8"></script>
  </head>
  <body>
    <h2>Hey!</h2>
  </body>
</html>

app.js 파일도 만들어야 합니다.

Running the Server

이제, 서버를 실행하세요.

webpack serve --host 0.0.0.0 --client-web-socket-url ws://10.10.10.61:8080/ws --watch-options-poll

기본적으로, 서버는 로컬 호스트에서만 접근할 수 있습니다. 호스트 PC에서 접근할 것이므로, 이를 허용하려면 --host를 변경해야 합니다.

webpack-dev-server는 파일이 변경될 때 다시 로드하기 위해 WebSocket에 연결하는 스크립트를 번들에 포함합니다. --client-web-socket-url 플래그는 스크립트가 WebSocket을 찾을 위치를 알고 있는지 확인합니다. 서버는 기본적으로 8080 포트를 사용하므로, 여기에서도 지정해야 합니다.

--watch-options-poll은 webpack이 파일의 변경을 감지할 수 있도록 합니다. 기본적으로, webpack은 파일 시스템에 의해 트리거되는 이벤트를 수신하지만, VirtualBox에는 이와 관련된 많은 문제가 있습니다.

서버는 이제 http://10.10.10.61:8080에서 접근할 수 있습니다. app.js를 변경하면 실시간으로 다시 로드됩니다.

Advanced Usage with nginx

좀 더 생산적인 환경을 모방하기 위해, nginx로 webpack-dev-server를 프록시 할 수도 있습니다.

nginx 설정 파일에 다음을 추가하십시오.

server {
  location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    error_page 502 @start-webpack-dev-server;
  }

  location @start-webpack-dev-server {
    default_type text/plain;
    return 502 "Please start the webpack-dev-server first.";
  }
}

proxy_set_header 줄은 WebSocket이 올바르게 작동하도록 허용하기 때문에 중요합니다.

webpack-dev-server를 시작하는 명령을 다음과 같이 변경할 수 있습니다.

webpack serve --client-web-socket-url ws://10.10.10.61:8080/ws --watch-options-poll

이렇게 하면, 127.0.0.1에서만 서버에 접근할 수 있으며, nginx가 호스트 PC에서 사용할 수 있도록 처리하므로 괜찮습니다.

Conclusion

고정 IP에서 Vagrant box에 접근할 수 있도록 만든 다음, webpack-dev-server를 공개적으로 접근할 수 있도록 하여 브라우저에서 접근할 수 있도록 했습니다. VirtualBox가 파일 시스템 이벤트를 보내지 않아 서버가 파일 변경 시 다시 로드되지 않는 일반적인 문제를 해결했습니다.

Dependency Management

Dynamic expressions in import() or require()

요청에 표현식이 포함된 경우 컨텍스트가 생성되므로, 컴파일 시간에 정확한 모듈을 알 수 없습니다.

예를 들어, .ejs 파일을 포함하는 다음과 같은 폴더 구조가 있습니다.

example_directory
└── template/
    ├── table.ejs
    ├── table-row.ejs
    └── directory/
        └── another.ejs

import() 또는 require() 호출이 평가될 때:

import(`./template/${name}.ejs`);
require(`./template/${name}.ejs`);

Webpack은 import() 또는 require() 호출을 분석하여 몇 가지 정보를 추출합니다.

Directory: ./template
Regular expression: /^.*\.ejs$/

컨텍스트 모듈

컨텍스트 모듈이 생성됩니다. 정규 표현식과 일치하는 요청에 필요할 수 있는 해당 디렉터리의 모든 모듈에 대한 참조를 포함합니다. 컨텍스트 모듈은 요청을 모듈 id로 변환하는 맵을 포함합니다.

맵 예제:

{
  "./table.ejs": 42,
  "./table-row.ejs": 43,
  "./directory/another.ejs": 44
}

컨텍스트 모듈은 또한 맵에 접근하기 위한 런타임 로직도 포함합니다.

이는 동적 호출이 지원되지만, 일치하는 모든 모듈이 번들에 포함된다는 것을 의미합니다.

import.meta.webpackContext

The ESM equivalent of require.context is import.meta.webpackContext.

import.meta.webpackContext(directory, {
  recursive: true,
  regExp: /^\.\/.*$/,
  mode: "sync",
});

require.context

require.context() 함수로 자신만의 컨텍스트를 만들 수 있습니다.

검색할 디렉터리, 하위 디렉터리를 검색해야 하는지 여부를 나타내는 플래그, 일치하는 파일의 정규식을 전달할 수 있습니다.

Webpack은 빌드 하는 동안 코드에서 require.context()를 구문 분석합니다.

구문은 다음과 같습니다.

require.context(
  directory,
  (useSubdirectories = true),
  (regExp = /^\.\/.*$/),
  (mode = "sync"),
);

예:

require.context("./test", false, /\.test\.js$/);
// test 디렉터리에서 요청이 `.test.js`로 끝나는 파일이 있는 컨텍스트입니다.
require.context("../", true, /\.stories\.js$/);
// 상위 폴더와 그 하위 폴더에서 `.stories.js`로 끝나는 파일이 있는 컨텍스트입니다.

context module API

컨텍스트 모듈은 하나의 인수(요청)를 가지는 함수를 export 합니다.

export된 함수는 resolve, keys, id 3가지 속성을 가집니다.

  • resolve는 파싱된 요청의 모듈 id를 반환하는 함수입니다.
  • keys는 컨텍스트 모듈이 처리할 수 있는 가능한 모든 요청의 배열을 반환하는 함수입니다.

이것은 디렉터리의 모든 파일을 요청하거나 패턴과 일치시키려는 경우에 유용할 수 있습니다. 예제는 다음과 같습니다.

function importAll(r) {
  r.keys().forEach(r);
}

importAll(
  import.meta.webpackContext("../components/", {
    recursive: true,
    regExp: /\.js$/,
  }),
);
const cache = {};

function importAll(r) {
  for (const key of r.keys()) cache[key] = r(key);
}

importAll(
  import.meta.webpackContext("../components/", {
    recursive: true,
    regExp: /\.js$/,
  }),
);
// 빌드 시 캐시에는 필요한 모든 모듈이 채워집니다.
  • id는 컨텍스트 모듈의 모듈 ID입니다. 이는 import.meta.webpackHot.accept 또는 module.hot.accept에 유용할 수 있습니다.

Installation

이 가이드에서는 webpack을 설치하는 데 사용되는 다양한 방법에 대해 설명합니다.

Prerequisites

시작하기 전에, Node.js가 최신 버전으로 설치되어 있는지 확인하세요. 현재 장기 지원 버전(LTS)은 이상적인 시작점입니다. 이전 버전에서는 webpack 혹은 관련 패키지에 필요한 기능이 누락되어 있을 수 있기 때문에 다양한 문제가 발생할 수 있습니다.

Local Installation

최신 webpack 릴리스는 다음과 같습니다.

GitHub release

최신 릴리스 또는 특정 버전을 설치하려면 다음 명령 중 하나를 실행하세요.

npm install --save-dev webpack
# 또는 특정 버전
npm install --save-dev webpack@<version>

webpack v4 이상을 사용하는 경우 CLI도 설치해야 합니다.

npm install --save-dev webpack-cli

대부분의 프로젝트에서는 로컬 설치를 권장합니다. 이를 통해 주요 변경사항이 있을 때 개별적으로 프로젝트를 쉽게 업그레이드 할 수 있습니다. 일반적으로 webpack은 하나 이상의 npm scripts를 통해 실행되며 이 스크립트는 로컬의 node_modules 디렉터리에 설치된 webpack을 찾습니다.

"scripts": {
  "build": "webpack --config webpack.config.js"
}

Global Installation

다음과 같이 NPM을 설치하면 webpack을 전역적으로 사용할 수 있습니다.

npm install --global webpack

Bleeding Edge

webpack이 제공하는 최신 버전을 사용하는 데 관심이 있다면 다음 명령을 사용하여 베타 버전을 설치하거나 webpack 저장소에서 직접 설치해 볼 수 있습니다.

npm install --save-dev webpack@next
# 또는 특정 tagname/branchname
npm install --save-dev webpack/webpack#<tagname/branchname>

Hot Module Replacement

Hot Module Replacement(또는 HMR)는 webpack에서 제공하는 가장 유용한 기능 중 하나입니다. 모든 종류의 모듈을 새로고침 할 필요 없이 런타임에 업데이트 할 수 있습니다. 이 페이지는 구현에 초점을 맞추고 개념 페이지는 작동 원리와 왜 유용한지에 대한 자세한 내용을 제공합니다.

Enabling HMR

이 기능은 생산성에 많은 도움을 줍니다. webpack-dev-server 설정을 업데이트하고 webpack의 내장 HMR 플러그인을 사용하면 됩니다. index.js 모듈에서 사용될 것이므로 print.js의 엔트리 포인트도 제거합니다.

webpack-dev-server v4.0.0부터 Hot Module Replacement가 기본적으로 활성화되어 있습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: {
       app: './src/index.js',
-      print: './src/print.js',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
+     hot: true,
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

HMR에 대한 수동 엔트리포인트를 제공할 수도 있습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';
+ import webpack from 'webpack';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: {
       app: './src/index.js',
-      print: './src/print.js',
+      // hot module replacement를 위한 런타임 코드
+      hot: 'webpack/hot/dev-server.js',
+      // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
+      client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
+     // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
+     hot: false,
+     client: false,
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
+     // hot module replacement를 위한 플러그인
+     new webpack.HotModuleReplacementPlugin(),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

이제 index.js 파일을 업데이트하여 print.js 내부의 변경이 감지되면 webpack에서 업데이트된 모듈을 수락하도록 지시합니다.

index.js

  import _ from 'lodash';
  import printMe from './print.js';

  function component() {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;

    element.appendChild(btn);

    return element;
  }

  document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }

print.js에서 console.log 문을 변경하면 브라우저 콘솔에 다음과 같은 출력이 표시됩니다. (당분간 button.onclick = printMe 출력에 대해 걱정하지 마세요. 나중에 해당 부분을 변경할 것입니다.)

print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   console.log('Updating print.js...');
  }

console

[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR]  - 20

Via the Node.js API

Node.js API와 함께 Webpack Dev Server를 사용하는 경우 webpack 설정 객체에 dev 서버 옵션을 추가하지 마십시오. 대신 생성 시 두 번째 매개 변수로 전달하십시오. 예를 들어 보겠습니다.

new WebpackDevServer(options, compiler)

HMR을 활성화하려면 HMR 엔트리 포인트를 포함하도록 webpack 설정 객체도 수정해야 합니다. 다음은 그 모습에 대한 간단한 예시입니다.

dev-server.js

import path from "node:path";
import { fileURLToPath } from "node:url";
import HtmlWebpackPlugin from "html-webpack-plugin";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const config = {
  mode: "development",
  entry: [
    // hot module replacement를 위한 런타임 코드
    "webpack/hot/dev-server.js",
    // 웹 소켓 전송, hot 및 live 리로드 로직을 위한 개발 서버 클라이언트
    "webpack-dev-server/client/index.js?hot=true&live-reload=true",
    // 엔트리
    "./src/index.js",
  ],
  devtool: "inline-source-map",
  plugins: [
    // hot module replacement를 위한 플러그인
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      title: "Hot Module Replacement",
    }),
  ],
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};
const compiler = webpack(config);

// `hot` 및 `client` 옵션을 수동으로 추가했기 때문에 비활성화됩니다.
const server = new WebpackDevServer({ hot: false, client: false }, compiler);

try {
  await server.start();
  console.log("dev server is running");
} catch (err) {
  throw new Error(`Failed to start dev server: ${err.message}`, { cause: err });
}

webpack-dev-server Node.js API 전체 문서를 참고하세요.

Gotchas

Hot Module Replacement는 까다로울 수 있습니다. 이를 보여주기 위해 작업 예제로 돌아갑시다. 계속해서 예제 페이지의 버튼을 클릭하면 콘솔이 이전 printMe 함수를 인쇄하고 있음을 알 수 있습니다.

이것은 버튼의 onclick 이벤트 핸들러가 여전히 원래의 printMe 함수에 바인딩 되어 있기 때문에 발생합니다.

HMR에서 이 작업을 수행하려면 module.hot.accept를 사용하여 새 printMe 함수에 대한 바인딩을 업데이트해야 합니다.

index.js

  import _ from 'lodash';
  import printMe from './print.js';

  function component() {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;  // onclick 이벤트는 원래 printMe 함수에 바인딩 됩니다.

    element.appendChild(btn);

    return element;
  }

- document.body.appendChild(component());
+ let element = component(); // print.js 변경 시 다시 렌더링할 요소 저장
+ document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
-     printMe();
+     document.body.removeChild(element);
+     element = component(); // 클릭 핸들러를 업데이트하려면 "component"를 다시 렌더링하십시오.
+     document.body.appendChild(element);
    })
  }

이것은 하나의 예시일 뿐이지만 사람들이 실수할 수 있는 상황이 많이 있습니다. 운 좋게도 Hot Module Replacement를 훨씬 쉽게 만들어주는 많은 로더가 있습니다. 그중 일부는 아래에 언급되었습니다.

HMR with Stylesheets

CSS Hot Module Replacement는 실제로 style-loader의 도움으로 상당히 간단합니다. 이 로더는 CSS 의존성이 업데이트될 때 <style>태그를 패치하기 위해 백그라운드에서 module.hot.accept를 사용합니다.

먼저 다음 명령으로 두 로더를 모두 설치해 보겠습니다.

npm install --save-dev style-loader css-loader

이제 로더를 사용하도록 설정 파일을 업데이트 하겠습니다.

webpack.config.js

  import path from 'node:path';
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from 'html-webpack-plugin';

  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  export default {
    entry: {
      app: './src/index.js',
    },
    devtool: 'inline-source-map',
    devServer: {
      static: './dist',
      hot: true,
    },
+   module: {
+     rules: [
+       {
+         test: /\.css$/,
+         use: ['style-loader', 'css-loader'],
+       },
+     ],
+   },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement',
      }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

스타일 시트 핫 로딩은 모듈로 가져오는 것만큼 쉽습니다.

project

  webpack-demo
  ├── package.json
  ├── webpack.config.js
  ├── /dist
  │   └── bundle.js
  └── /src
      ├── index.js
      ├── print.js
+     └── styles.css

styles.css

body {
  background: blue;
}

index.js

  import _ from 'lodash';
  import printMe from './print.js';
+ import './styles.css';

  function component() {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;  // onclick 이벤트는 원래 printMe 함수에 바인딩 됩니다.

    element.appendChild(btn);

    return element;
  }

  let element = component();
  document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
      document.body.removeChild(element);
      element = component(); // 클릭 핸들러를 업데이트하려면 "component"를 다시 렌더링하십시오.
      document.body.appendChild(element);
    })
  }

body의 스타일을 background : red;로 변경하면 새로고침 없이도 페이지의 배경색이 변경되는 것을 즉시 확인할 수 있습니다.

styles.css

  body {
-   background: blue;
+   background: red;
  }

Other Code and Frameworks

HMR이 다양한 프레임워크 및 라이브러리와 원활하게 상호 작용할 수 있도록 커뮤니티에는 다른 많은 로더와 예제가 있습니다.

  • React Hot Loader: 실시간으로 React 컴포넌트를 조정
  • Vue Loader: Vue 캄포넌트에 대한 HMR을 즉시 지원하는 로더
  • Elm Hot webpack Loader: Elm 프로그래밍 언어에 대한 HMR 지원
  • Angular HMR: 로더가 필요 없습니다! HMR 지원은 Angular CLI에 내장되어 있으며, --hmr플래그를 ng serve 명령에 추가하면 됩니다.
  • Svelte Loader: Svelte 컴포넌트에 대한 HMR을 즉시 지원하는 로더

Tree Shaking

Tree shaking은 사용되지 않는 코드를 제거하기 위해 JavaScript 컨텍스트에서 일반적으로 사용되는 용어입니다. ES2015 모듈 구문은 정적 구조에 의존합니다. 예를 들면, importexport가 있습니다. 이름과 개념은 ES2015 모듈 번들러의 rollup에 의해 대중화되었습니다.

webpack 2 릴리스에서는 ES2015 모듈(별칭 harmony 모듈)과 사용하지 않는 모듈의 export를 감지하는 기능을 제공합니다. 새로운 webpack 4의 릴리스는 package.json"sideEffects" 프로퍼티를 통해 컴파일러에 힌트를 제공하는 방식으로 기능을 확장합니다. 프로젝트의 어떤 파일이 "순수"한지 나타내며, 만약 사용하지 않는다면 제거해도 괜찮은지 표시합니다.

Add a Utility

다음 두 함수를 내보내는 새 유틸리티 파일인 src/math.js를 프로젝트에 추가해 보겠습니다.

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  │   ├── bundle.js
  │   └── index.html
  ├── /src
  │   ├── index.js
+ │   └── math.js
  └── /node_modules

src/math.js

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

mode 옵션을 development로 설정하여 번들이 압축되지 않도록 합니다.

webpack.config.js

import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
+ mode: 'development',
+ optimization: {
+   usedExports: true,
+ },
};

이를 통해 새로운 메소드 중 하나를 사용하도록 entry 스크립트를 업데이트하고, 스크립트를 간단하게 하기 위해 lodash를 삭제하겠습니다.

src/index.js

- import _ from 'lodash';
+ import { cube } from './math.js';

  function component() {
-   const element = document.createElement('div');
+   const element = document.createElement('pre');

-   // 이제 Lodash를 스크립트로 가져왔습니다.
-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.innerHTML = [
+     'Hello webpack!',
+     '5 cubed is equal to ' + cube(5)
+   ].join('\n\n');

    return element;
  }

  document.body.appendChild(component());

우리는 src/math.js 모듈에서 square 메소드를 가져오지 않았습니다. 이 함수는 "사용하지 않는 코드"로 알려져 있고, 사용하지 않아 삭제되어야 하는 export를 의미합니다. 이제 npm 스크립트인 npm run build를 실행하여 출력된 번들을 살펴보겠습니다.

dist/bundle.js (around lines 90 - 100)

/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
  "use strict";

  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__.a = cube;
  function square(x) {
    return x * x;
  }

  function cube(x) {
    return x * x * x;
  }
});

위의 unused harmony export square 주석을 참고하세요. 아래 코드를 보면 square를 가져오지 않지만, 여전히 번들에 포함되어 있습니다. 다음 섹션에서 수정해 보겠습니다.

Mark the file as side-effect-free

100% ESM 모듈에서는 사이드 이펙트를 쉽게 식별할 수 있습니다. 그러나 우리는 아직 거기까지는 도달하지 않았으므로, 도달하기 까지는 코드의 "순수성"에 대한 힌트를 webpack 컴파일러에 제공해야 합니다.

이를 수행하는 방법은 package.json의 "sideEffects" 속성입니다.

{
  "name": "your-project",
  "sideEffects": false
}

위에 언급한 코드는 사이드 이펙트를 포함하지 않으므로, 간단하게 false로 프로퍼티를 표시하여 사용하지 않는 export는 제거해도 괜찮다는 것을 webpack에 알릴 수 있습니다.

코드에 사이드 이펙트가 있다면 대신 배열을 사용할 수 있습니다.

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js"]
}

배열은 관련된 파일의 간단한 전역 패턴을 허용합니다. 내부적으로 glob-to-regexp을 사용합니다 (사용 가능: *, **, {a,b}, [a-z]). /을 포함하지 않는 *.css와 같은 패턴은 **/*.css처럼 취급합니다.

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

마지막으로 "sideEffects"module.rules 옵션으로도 설정할 수 있습니다.

Clarifying tree shaking and sideEffects

sideEffectsusedExports(트리 쉐이킹으로 알려져 있음)의 최적화는 두 가지 다른 점이 있습니다.

sideEffects 전체 모듈 및 파일, 전체 하위 트리를 건너뛸 수 있기 때문에 훨씬 더 효율적입니다.

usedExportsterser를 사용하여 문장에서 사이드 이펙트를 감지합니다. 이것은 JavaScript에서 어려운 작업이며 간단한 sideEffects 플래그만큼 효과적이지 않습니다. 또한 사이드 이펙트를 확인해야 하는 명세가 있기 때문에 하위트리 및 의존성을 무시할 수 없습니다. export 기능은 잘 동작하지만, React의 Higher Order Components(HOC)는 이와 관련된 문제가 있습니다.

동적 import()를 사용하는 경우 webpackExports 매직 주석을 사용하여 노출해야 하는 내보내기를 지정할 수 있으며, 이렇게 하면 webpack이 나머지 내보내기를 자동으로 처리합니다. 자세한 내용은 매직 주석을 참고하세요.

예를 들어보겠습니다.

import { Button } from "@shopify/polaris";

미리 번들된 버전은 아래와 같습니다.

import hoistStatics from "hoist-non-react-statics";

function Button(_ref) {
  // ...
}

function merge() {
  const _final = {};

  for (
    let _len = arguments.length, objs = Array.from({ length: _len }), _key = 0;
    _key < _len;
    _key++
  ) {
    objs[_key] = arguments[_key];
  }

  for (let _i = 0, _objs = objs; _i < _objs.length; _i++) {
    const obj = _objs[_i];
    mergeRecursively(_final, obj);
  }

  return _final;
}

function withAppProvider() {
  return function addProvider(WrappedComponent) {
    const WithProvider =
      /*#__PURE__*/
      (function (_React$Component) {
        // ...
        return WithProvider;
      })(Component);

    WithProvider.contextTypes = WrappedComponent.contextTypes
      ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes)
      : polarisAppProviderContextTypes;
    const FinalComponent = hoistStatics(WithProvider, WrappedComponent);
    return FinalComponent;
  };
}

const Button$1 = withAppProvider()(Button);

export {
  // ...,
  Button$1,
};

Button이 사용되지 않으면 export { Button$1 };을 효과적으로 제거하고 나머지 코드를 모두 남길 수 있습니다. "이 코드가 사이드 이펙트가 없거나 안전하게 삭제할 수 있을까요?"라는 질문을 할 수 있습니다. withAppProvider()(Button) 라인 때문에 말하기 어렵습니다. withAppProvider가 호출되고 리턴 값도 호출됩니다. merge 또는 hoistStatics를 호출할 때 사이드 이펙트가 있나요? WrappedComponent.contextTypes (Getter?)를 읽거나 WithProvider.contextTypes (Setter?)를 할당할 때 사이드 이펙트가 있나요?

Terser는 알아내려고 노력하지만 여러 상황에서 장담할 수는 없습니다. 이것은 terser가 알아낼 수 없기 때문에, terser가 역할을 잘 수행하지 못한다는 것이 아닙니다. JavaScript 같은 동적 언어에서 확실하게 판단하는 것은 매우 어렵습니다.

그러나 /*#__PURE__*/ 어노테이션을 이용하여 terser를 도와줄 수 있습니다. 그 구문은 사이드 이펙트가 없는 것으로 표시합니다. 그래서 간단한 변경만으로 코드를 tree-shake 할 수 있습니다.

const Button$1 = /* #__PURE__ */ withAppProvider()(Button);

이렇게 하면 이 코드를 제거 할 수 있습니다. 그러나 포함되어야 하거나 평가가 필요한 import는 사이드 이펙트가 있을 수 있기 때문에 여전히 이에 대한 문제가 남아 있습니다.

이 문제를 해결하기 위해 package.json"sideEffects" 프로퍼티를 사용합니다.

이것은 /*#__PURE__*/와 비슷하지만, 구문 레벨이 아닌 모듈 레벨에서 사용합니다. "sideEffects" 프로퍼티에 대해 "sideEffect가 없다고 플래그된 모듈에서 직접적인 export가 없는 경우 번들러는 사이드 이펙트에 대한 평가를 건너 뛸 수 있다."라고 설명하고 있습니다.

Shopify's Polaris 예시에서 원래 모듈은 다음과 같습니다.

index.js

import "./configure";

export * from "./types";
export * from "./components";

components/index.js

// ...
export { default as Breadcrumbs } from "./Breadcrumbs";
export { buttonFrom, buttonsFrom, default as Button } from "./Button";
export { default as ButtonGroup } from "./ButtonGroup";
// ...

package.json

// ...
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],
// ...

import { Button } from "@shopify/polaris";는 다음과 같이 동작합니다.

  • 포함: 모듈을 포함하고 평가하며 계속 의존성을 분석합니다.
  • 건너뛰기: 포함하지 않으며 평가하지 않으나 계속 의존성을 평가합니다.
  • 제외: 포함하지 않으며 평가하지 않고 의존성도 분석하지 않습니다.

매칭되는 리소스별로 자세히 보겠습니다.

  • index.js: 직접 export하여 사용하진 않지만 sideEffect의 플래그는 사용 -> 포함
  • configure.js: export하여 사용되지 않지만 sideEffect의 플래그는 사용 -> 포함
  • types/index.js: export하여 사용되지 않고 sideEffect로 플래그도 사용하지 않음 -> 제외
  • components/index.js: 직접 export하여 사용하지 않고 sideEffect로 플래그도 사용하지 않음, 그러나 다시 export한 export는 사용됨 -> 건너 뜀
  • components/Breadcrumbs.js: export하여 사용되지 않고 sideEffect로 플래그도 사용하지 않음 -> 제외 sideEffect 플래그가 있더라도 components/Breadcrumbs.css와 같은 모든 의존성은 제외됩니다.
  • components/Button.js: 직접 export를 사용하고 sideEffect 플래그는 사용하지 않음 -> 포함
  • components/Button.css: 직접 export를 사용하지 않지만 sideEffect 플래그는 사용함 ->포함

위의 경우에 4개 모듈만 번들에 포함됩니다.

  • index.js: 거의 없음
  • configure.js
  • components/Button.js
  • components/Button.css

이 최적화 후, 다른 최적화도 적용할 수 있습니다. 예를 들면, buttonFromButton.js에서 export하는 buttonsFrom은 사용되지 않습니다. usedExports 최적화는 이를 알아채고 terser는 모듈에서 일부 명령문을 삭제할 수 있습니다.

모듈의 연결에도 적용됩니다. 따라서 이 4개의 모듈과 엔트리 모듈(그리고 아마도 좀 더 많은 의존성)을 연결할 수 있습니다. 결국 index.js에는 생성되는 코드가 없습니다.

Full Example: Understanding Side Effects with CSS Files

sideEffects 플래그의 영향을 더 잘 이해하기 위해 CSS 에셋이 포함된 npm 패키지의 전체 예제와 트리 셰이킹 중에 이러한 에셋이 어떻게 영향을 받을 수 있는지 살펴보겠습니다. "awesome-ui"라는 가상의 UI 컴포넌트 라이브러리를 만들어 보겠습니다.

Package Structure

예제 패키지는 다음과 같습니다.

awesome-ui/
├── package.json
└── dist/
    ├── index.js
    ├── components/
    │   ├── index.js
    │   ├── Button/
    │   │   ├── index.js
    │   │   └── Button.css
    │   ├── Card/
    │   │   ├── index.js
    │   │   └── Card.css
    │   └── Modal/
    │       ├── index.js
    │       └── Modal.css
    └── theme/
        ├── index.js
        └── defaultTheme.css

Package Files Content

package.json

{
  "name": "awesome-ui",
  "version": "1.0.0",
  "main": "dist/index.js",
  "sideEffects": false
}

dist/index.js

export * from "./components";
export * from "./theme";

dist/components/index.js

export { default as Button } from "./Button";
export { default as Card } from "./Card";
export { default as Modal } from "./Modal";

dist/components/Button/index.js

import "./Button.css"; // 이것은 사이드 이펙트가 있습니다. 가져올 때 스타일이 적용됩니다!

export default function Button(props) {
  // Button component 구현
  return {
    type: "button",
    ...props,
  };
}

dist/components/Button/Button.css

.awesome-ui-button {
  background-color: #0078d7;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

dist/components/Card/index.js and dist/components/Modal/index.js would have similar structure.

dist/theme/index.js

import "./defaultTheme.css"; // 이것은 사이드 이펙트가 있습니다!

export const themeColors = {
  primary: "#0078d7",
  secondary: "#f3f2f1",
  danger: "#d13438",
};

What Happens When Consuming This Package?

이제 Button 구성 요소만 사용하려는 컨수머 애플리케이션을 상상해 보세요.

import { Button } from "awesome-ui";

// 버튼 컴포넌트를 사용하세요

With sideEffects: false in package.json

트리 쉐이킹이 활성화된 상태에서 webpack이 이 가져오기를 처리하는 경우.

  1. 버튼에 대한 가져오기만 확인됩니다.
  2. package.json을 보면 sideEffects: false가 보입니다.
  3. Button 구성 요소 코드만 포함하면 된다고 판단합니다.
  4. 모든 파일에 사이드 이펙트가 없는 것으로 표시되어 있으므로 단지 버튼에 대한 JavaScript 코드만 포함됩니다.
  5. CSS 파일 가져오기가 중단됩니다! Button.css가 Button/index.js로 가져와졌더라도 webpack은 이 가져오기에 사이드 이펙트가 없다고 가정합니다.

결과: Button 구성 요소는 렌더링되지만, 트리 셰이킹 중에 Button.css가 제거되었기 때문에 스타일이 적용되지 않습니다.

The Correct Configuration for This Package

이 문제를 해결하려면 package.json을 업데이트하여 CSS 파일에 사이드 이펙트가 있음을 올바르게 표시해야 합니다.

{
  "name": "awesome-ui",
  "version": "1.0.0",
  "main": "dist/index.js",
  "sideEffects": ["**/*.css"]
}

이 설정을 사용하면,

  1. Webpack은 여전히 Button 구성 요소만 필요하다고 식별합니다.
  2. 하지만 이제 CSS 파일에는 사이드 이펙트가 있다는 것을 인식합니다.
  3. 따라서 Button/index.js를 처리할 때 Button.css가 포함됩니다.

The Decision Tree for Side Effects

트리 쉐이킹 중에 WebPack이 모듈을 평가하는 방식은 다음과 같습니다.

  1. 이 모듈의 내보내기 기능은 직접 사용되나요, 아니면 간접적으로 사용되나요?

    • 예: 모듈을 포함합니다.
    • 아니요: 2단계로 계속 진행하세요.
  2. 모듈에 사이드 이펙트가 표시되어 있나요?

    • 예(sideEffects가 이 파일을 포함하거나 true인 경우): 모듈을 포함합니다.
    • 아니요(sideEffectsfalse이거나 이 파일을 포함하지 않는 경우): 모듈과 해당 종속성을 제외합니다.

적절한 sideEffects 구성을 갖춘 라이브러리 파일의 경우:

  • dist/index.js: 직접 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 건너뛰기
  • dist/components/index.js: 직접 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 건너뛰기
  • dist/components/Button/index.js: 직접 내보내기 사용 -> 포함
  • dist/components/Button/Button.css: 내보내기가 불가능하고 사이드 이펙트 있음 -> 포함
  • dist/components/Card/*: 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 제외
  • dist/components/Modal/*: 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 제외
  • dist/theme/*: 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 제외

Real-World Impact

잘못된 사이드 이펙트 설정은 심각한 영향을 미칠 수 있습니다.

  1. CSS 미포함: 컴포넌트가 스타일 없이 렌더링됨
  2. 전역 JavaScript 미실행: 폴리필 또는 전역 구성이 실행되지 않음
  3. 초기화 코드 생략: 컴포넌트를 등록하거나 이벤트 리스너를 설정하는 함수가 실행되지 않음

이러한 문제는 트리 쉐이킹이 활성화된 프로덕션 빌드에서만 발생하는 경우가 많기 때문에 디버깅하기가 특히 어려울 수 있습니다.

Testing Side Effects Configuration

사이드 이펙트 설정이 올바른지 테스트하는 좋은 방법은 다음과 같습니다.

  1. 하나의 컴포넌트만 가져오는 최소 애플리케이션을 만듭니다.
  2. 프로덕션 설정(트리 쉐이킹 활성화)으로 빌드합니다.
  3. 모든 필수 스타일과 동작이 제대로 작동하는지 확인합니다.
  4. 생성된 번들을 보고 올바른 파일이 포함되어 있는지 확인합니다.

Mark a function call as side-effect-free

/*#__PURE__*/어노테이션을 사용하여 해당 함수 호출이 사이드 이펙트가 없다(side-effect-free)(순수하다)는 것을 webpack에 알릴 수 있습니다. 함수 호출 앞에 추가하여 사이드 이펙트가 없는 것으로 표시할 수 있습니다. 함수에 전달된 인수는 어노테이션으로 표시되지 않고 개별적으로 표시해야 할 수 있습니다. 사용하지 않는 변수의 초기값이 사이드 이펙트가 없다(순수하다)면, 사용하지 않는 코드로 표시되고 실행되지 않으며 최소화할 때 삭제됩니다. 이런 동작은 optimization.innerGraphtrue 일 때 활성화됩니다.

file.js

/* #__PURE__ */ double(55);

Mark a function declaration as side-effect-free

5.107.0+

Webpack은 함수 선언을 pure로 표시하기 위한 #__NO_SIDE_EFFECTS__ annotation도 지원합니다. 이렇게 annotation이 붙은 함수는 반환값이 사용되지 않을 때, 함수 본문이 정적으로 pure하다고 분석될 수 없는 경우에도 해당 호출이 번들에서 제거될 수 있습니다. 이는 원래 각 호출 지점마다 /*#__PURE__*/ annotation이 필요했을 factory 함수나 builder 함수에 특히 유용합니다.

// utils.js
/*#__NO_SIDE_EFFECTS__*/
export function createLogger(prefix) {
  return (msg) => console.log(`[${prefix}] ${msg}`);
}
// app.js
import { createLogger } from "./utils";

// `createLogger`에 annotation이 있고 반환값이 사용되지 않으므로 제거됩니다.
const unused = createLogger("debug");

Minify the Output

importexport 구문을 통해 "사용하지 않는 코드"를 삭제했습니다. 하지만 번들에서도 삭제해야 합니다. 이렇게 하려면 mode 옵션을 production으로 설정해야 합니다.

webpack.config.js

import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
- mode: 'development',
- optimization: {
-   usedExports: true,
- }
+ mode: 'production',
};

즉, 다른 npm run build를 실행하고 변경된 사항이 있는지 볼 수 있습니다.

dist/bundle.js에서 다른 점을 찾았나요? 정확하게 전체 번들이 최소화되고 난독화 되었지만, 자세히 보면 포함되어 있던 square 함수가 없으며 난독화 된 cube 함수 (function r(e){return e*e*e}n.a=r)를 볼 수 있습니다. 최소화와 tree shaking으로 번들은 이제 몇 바이트 더 작아졌습니다! 위의 임의로 만든 예제에서는 큰 변화를 느끼지 못하겠지만 tree shaking은 복잡한 의존성 트리가 있는 커다란 애플리케이션에서 작업할 때 번들의 크기를 많이 줄일 수 있습니다.

Common Pitfalls with Side Effects

트리 쉐이킹과 sideEffects 플래그를 사용할 때 피해야 할 몇 가지 일반적인 함정이 있습니다.

1. Over-optimistic sideEffects: false

package.json 파일에서 sideEffects: false를 설정하면 최적의 트리 셰이킹 효과를 얻을 수 있지만, 코드에 실제로 부작용이 있는 경우 문제가 발생할 수 있습니다. 숨겨진 부작용의 예는 다음과 같습니다.

  • CSS 가져오기(위에서 설명한 대로)
  • 전역 객체를 수정하는 폴리필
  • 글로벌 이벤트 리스너를 등록하는 라이브러리
  • 프로토타입 체인을 수정하는 코드

2. Re-exports with Side Effects

다음 패턴을 고려해 보세요.

// 이 파일에는 건너뛸 수 있는 부작용이 있습니다
import "./polyfill";

// component를 다시 export합니다.
export * from "./components";

소비자가 특정 구성 요소만 가져오는 경우, 부작용에 대한 적절한 표시가 없으면 폴리필 가져오기가 완전히 건너뛰어질 수 있습니다.

3. Forgetting about Nested Dependencies

패키지가 부작용을 올바르게 표시하더라도, 부작용을 잘못 표시하는 타사 패키지에 의존하는 경우 여전히 문제가 발생할 수 있습니다.

4. Testing Only in Development Mode

트리 쉐이킹은 일반적으로 프로덕션 모드에서만 완전히 활성화됩니다. 개발 모드에서만 테스트하면 배포될 때까지 트리 쉐이킹 문제를 숨길 수 있습니다.

Conclusion

그래서 tree shaking의 이점을 살리기 위하여

  • ES2015 모듈 구문을 사용해야 하는 것을 배웠습니다. (예: importexport)
  • 컴파일러가 ES2015 모듈 구문을 CommonJS 모듈로 변환하지 않도록 해야 합니다. (이것은 인기 있는 Babel preset @babel/preset-env의 기본 동작입니다. 자세한 내용은 documentation를 참고하세요.)
  • package.json 파일에 "sideEffects" 속성을 추가하세요.
  • 특히 CSS 가져오기의 경우 사이드 이펙트가 있는 파일을 올바르게 표시하는 데 주의하세요.
  • 최소화와 tree shaking을 포함한 다양한 최적화를 사용하려면 production mode 설정 옵션을 사용하세요. (플래그 값을 사용하여 개발 모드에서 사이드 이펙트 최적화가 활성화됩니다)
  • production 모드에서는 일부 기능을 사용할 수 없으므로 devtool에 올바른 값을 설정했는지 확인하세요.

애플리케이션을 나무와 같이 생각할 수 있습니다. 실제로 사용되는 소스 코드와 라이브러리는 나무의 살아있는 잎과 같은 녹색을 나타냅니다. 사용하지 않는 코드는 가을에 바싹 마른 나무의 죽은 잎사귀처럼 갈색입니다. 낙엽을 없애기 위해서 나무를 흔들어서 낙엽을 떨어 뜨려야 합니다.

산출물에 대한 최적화에 더 관심이 있다면 production을 빌드하기 위한 상세 가이드로 이동하세요.

Production

이 가이드에서 프로덕션 사이트나 애플리케이션을 구축하기 위한 유틸리티와 좋은 사례들에 대해서 자세히 알아보겠습니다.

Setup

development와 production의 빌드 목표는 매우 다릅니다. development 에서는 강력한 소스 매핑, localhost 서버에서는 라이브 리로딩이나 hot module replacement 기능을 원합니다. production에서의 목표는 로드 시간을 줄이기 위해 번들 최소화, 가벼운 소스맵 및 애셋 최적화에 초점을 맞추는 것으로 변경됩니다. 논리적으로 분리를 해야 하면 일반적으로 환경마다 webpack 설정을 분리하여 작성하는 것이 좋습니다.

production과 development에 관련된 부분을 분리하더라도, 중복을 제거하기 위해 "공통"의 설정은 계속 유지해야 합니다. 이러한 설정을 합치기 위해 webpack-merge 유틸리티를 사용합니다. "공통"의 설정을 사용하면 환경별 설정에서 코드를 복사하지 않아도 됩니다.

webpack-merge를 설치하고 이전 가이드에서 이미 작업 한 부분을 분리하겠습니다.

npm install --save-dev webpack-merge

project

  webpack-demo
  ├── package.json
  ├── package-lock.json
- ├── webpack.config.js
+ ├── webpack.common.js
+ ├── webpack.dev.js
+ ├── webpack.prod.js
  ├── /dist
  ├── /src
  │   ├── index.js
  │   └── math.js
  └── /node_modules

webpack.common.js

+ import path from 'node:path';
+ import { fileURLToPath } from 'node:url';
+ import HtmlWebpackPlugin from 'html-webpack-plugin';
+
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = path.dirname(__filename);
+
+ export default {
+   entry: {
+     app: './src/index.js',
+   },
+   plugins: [
+     new HtmlWebpackPlugin({
+       title: 'Production',
+     }),
+   ],
+   output: {
+     filename: '[name].bundle.js',
+     path: path.resolve(__dirname, 'dist'),
+     clean: true,
+   },
+ };

webpack.dev.js

+ import { merge } from 'webpack-merge';
+ import common from './webpack.common.js';
+
+ export default merge(common, {
+   mode: 'development',
+   devtool: 'inline-source-map',
+   devServer: {
+     static: './dist',
+   },
+ });

webpack.prod.js

+ import { merge } from 'webpack-merge';
+ import common from './webpack.common.js';
+
+ export default merge(common, {
+   mode: 'production',
+ });

webpack.common.js에서는 entryoutput 설정을 구성하고, 두 환경 모두에 필요한 플러그인을 포함했습니다. webpack.dev.js에서는 modedevelopment로 설정했습니다. 또한 해당 환경에 권장되는 devtool(강력한 소스 매핑)과 devServer 설정도 추가했습니다. 마지막으로 webpack.prod.js에서는 modeproduction으로 설정하여, Tree shaking 가이드에서 처음 소개한 MinimizerPlugin을 로드합니다.

환경별 설정에서 merge()를 사용하여 호출하면 webpack.dev.jswebpack.prod.js에 공통 설정을 포함합니다. webpack-merge 툴은 병합을 위한 다양한 고급 기능을 제공하지만, 지금 사례에서는 이런 기능이 필요하지 않습니다.

NPM Scripts

지금부터 새로운 설정 파일을 사용하기 위해 npm 스크립트를 수정해 보겠습니다. webpack-dev-server를 실행하는 start 스크립트의 경우 webpack.dev.js를 사용하고, 프로덕션 빌드를 만들기 위해 webpack을 실행하는 build 스크립트의 경우 webpack.prod.js를 사용합니다.

package.json

  {
    "name": "development",
    "version": "1.0.0",
    "description": "",
    "main": "src/index.js",
    "scripts": {
-     "start": "webpack serve --open",
+     "start": "webpack serve --open --config webpack.dev.js",
-     "build": "webpack"
+     "build": "webpack --config webpack.prod.js"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "css-loader": "^7.1.3",
      "csv-loader": "^3.0.5",
      "express": "^5.2.1",
      "html-webpack-plugin": "^5.6.6",
      "style-loader": "^4.0.0",
      "webpack": "^5.105.0",
      "webpack-cli": "^7.0.0",
      "webpack-dev-middleware": "^8.0.3",
      "webpack-dev-server": "^5.2.3",
      "webpack-merge": "^6.0.1",
      "xml-loader": "^1.2.1"
    }
  }

production 설정을 계속 추가하는대로 출력이 어떻게 변경되는지 위 스크립트를 자유롭게 실행하여 확인해보세요.

Specify the Mode

많은 라이브러리는 process.env.NODE_ENV 변수를 이용하여 어떤 라이브러리를 포함해야 하는지 결정합니다. 예를 들어 process.env.NODE_ENV'production'으로 설정되지 않으면 몇몇 라이브러리는 디버깅의 편의성을 위해 로그 및 테스트를 추가할 수도 있습니다. 그러나 process.env.NODE_ENV'production'으로 설정되어 있으면 실제 사용자의 작업 실행 방식을 최적화하기 위해 코드의 중요한 부분을 추가하거나 삭제할 수 있습니다. webpack v4부터 mode를 지정하면 DefinePlugin을 통해 process.env.NODE_ENV가 자동으로 설정됩니다.

webpack.prod.js

  import { merge } from 'webpack-merge';
  import common from './webpack.common.js';

  export default merge(common, {
    mode: 'production',
  });

react와 같은 라이브러리를 사용한다면 DefinePlugin을 추가한 후에 명확하게 번들 크기가 줄어야 합니다. 또한 로컬 /src의 코드 역시 제어 할 수 있습니다. 따라서 다음 검사는 유효합니다.

src/index.js

  import { cube } from './math.js';
+
+ if (process.env.NODE_ENV !== 'production') {
+   console.log('Looks like we are in development mode!');
+ }

  function component() {
    const element = document.createElement('pre');

    element.innerHTML = [
      'Hello webpack!',
      '5 cubed is equal to ' + cube(5)
    ].join('\n\n');

    return element;
  }

  document.body.appendChild(component());

Minification

Webpack v4+의 production mode에서는 기본으로 코드를 최소화합니다.

MinimizerPlugin은 최소화를 시작하기에 좋고 기본으로도 사용되지만, 다른 선택지도 있습니다.

만약 다른 최소화 플러그인을 사용하기로 결정했다면, 다른 플러그인이 Tree shaking 가이드에 설명 된 대로 사용하지 않는 코드를 제거하고 optimization.minimizer를 제공하는지 확인해야 합니다.

Source Mapping

소스맵은 디버깅뿐만 아니라 벤치마크 테스트에도 유용하므로 프로덕션에도 활성화하는 것이 좋습니다. 즉, 프로덕션용으로 추천되는 빌드 속도가 가장 빠른 것을 선택해야 합니다. (devtool 참조) 이 가이드에서는 development에서 사용한 inline-source-map이 아닌 production의 source-map을 사용합니다.

webpack.prod.js

  import { merge } from 'webpack-merge';
  import common from './webpack.common.js';

  export default merge(common, {
    mode: 'production',
+   devtool: 'source-map',
  });

Minimize CSS

프로덕션을 위해 CSS를 최소화하는 것이 중요합니다. Minimizing for Production을 참고하세요.

CLI Alternatives

위에서 설명한 대부분의 옵션은 커맨드 라인 인자로 설정할 수 있습니다. 예를 들어 optimization.minimize은 --optimization-minimize, 그리고 mode는 --mode로 설정할 수 있습니다. CLI 인자의 전체 목록을 보려면 npx webpack --help=verbose를 실행하세요.

이런 간단한 방식은 편리하지만, 좀 더 알맞은 설정을 위해 webpack 설정 파일에서 이런 옵션을 설정하는 것이 좋습니다.

Lazy Loading

지연 로딩 또는 "온 디맨드" 로딩은 사이트나 애플리케이션을 최적화하는 좋은 방법입니다. 이 방법은 기본적으로 논리적인 중단점에서 코드를 분할한 다음 유저가 새로운 코드 블록을 요구하거나 필요로 하는 작업을 수행한 후 코드를 로딩하는 것입니다. 이렇게 하면 애플리케이션의 초기 로드 속도가 빨라지고 일부 블록이 로드되지 않을 수도 있어서 전체 무게가 줄어 듭니다.

Dynamic Import Example

코드 스플리팅의 예제를 가져와 이 개념을 더욱 잘 보여주기 위해 약간 수정해 보겠습니다. 이 코드는 별도의 청크인 lodash.bundle.js를 생성하고 스크립트가 실행되자마자 기술적으로 "지연 로드"됩니다. 문제는 번들을 로드하는데 유저 상호 작용이 필요하지 않다는 것입니다. 즉, 페이지가 로드 될 때마다 요청이 실행됩니다. 이것은 우리에게 큰 도움이 되지 않고 성능에 부정적인 영향을 미치게 됩니다.

다른 것을 시도해 봅시다. 유저가 버튼을 클릭 할 때 일부 텍스트를 콘솔에 기록하는 상호 작용을 추가합니다. 그러나 (print.js)를 로드하는 동안 처음 상호작용이 발생하기까지 기다려보겠습니다. 이를 위해 다시 돌아가서 코드 스플리팅의 final Dynamic Imports 예제를 다시 작업하고 메인 청크에 lodash를 남겨 둡니다.

프로젝트

webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 ├── /src
 │   ├── index.js
+│   └── print.js
 └── /node_modules

src/print.js

console.log(
  "The print.js module has loaded! See the network tab in dev tools...",
);

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};

src/index.js

+ import _ from 'lodash';
+
- async function getComponent() {
+ function component() {
    const element = document.createElement('div');
-   const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
+   const button = document.createElement('button');
+   const br = document.createElement('br');

+   button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.appendChild(br);
+   element.appendChild(button);
+
+   // Note that because a network request is involved, some indication
+   // of loading would need to be shown in a production-level site/app.
+   button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+     const print = module.default;
+
+     print();
+   });

    return element;
  }

- getComponent().then(component => {
-   document.body.appendChild(component);
- });
+ document.body.appendChild(component());

이제 webpack을 실행하고 새로운 지연 로딩 기능을 확인해 보겠습니다.

...
          Asset       Size  Chunks                    Chunk Names
print.bundle.js  417 bytes       0  [emitted]         print
index.bundle.js     548 kB       1  [emitted]  [big]  index
     index.html  189 bytes          [emitted]
...

Defer Import Example

어떤 경우에는 모듈의 모든 사용을 비동기로 변환하는 것이 귀찮거나 어려울 수 있습니다. 동기적 평가 작업만 지연시키는 기능을 제공하지 않고 모든 함수의 불필요한 비동기화를 강제하기 때문입니다.

TC39 제안모듈 평가 연기는 이 문제를 해결하기 위한 것입니다.

제안은 네임스페이스 특수한(exotic) 객체만 반환하는 새로운 구문 가져오기 형식을 갖추는 것입니다. 이 형식을 사용하면 모듈과 해당 종속성은 실행되지 않고, 모듈 그래프가 로드된 것으로 간주되기 전에 실행 준비가 완료될 때까지 완전히 로드됩니다.

이 모듈의 속성에 액세스할 때만 실행 작업이 수행됩니다(필요한 경우).

이 기능은 experiments.deferImport를 활성화하면 사용할 수 있습니다.

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
+ │   └── print.js
  └── /node_modules

src/print.js

console.log(
  "The print.js module has loaded! See the network tab in dev tools...",
);

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};

src/index.js

  import _ from 'lodash';
+ import defer * as print from './print';

  function component() {
    const element = document.createElement('div');
    const button = document.createElement('button');
    const br = document.createElement('br');

    button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.appendChild(br);
    element.appendChild(button);

-   // 네트워크 요청이 관련되어 있으므로
-   // 프로덕션 수준의 사이트/앱에서 로딩에 대한 표시가 표시되어야 합니다.
+   // 이 예에서 print 모듈은 다운로드되지만 평가되지 않습니다.
+   // 따라서 버튼을 클릭한 후에는 네트워크 요청이 발생하지 않습니다.
-   button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+   button.onclick = e => {
      const print = module.default;
+     //                  ^ 여기서 모듈을 평가합니다.

      print();
-   });
+   };

    return element;
  }

  getComponent().then(component => {
    document.body.appendChild(component);
  });
  document.body.appendChild(component());

이는 CommonJS 스타일의 지연 로딩과 유사합니다.

src/index.js

  import _ from 'lodash';
- import defer * as print from './print';

  function component() {
    const element = document.createElement('div');
    const button = document.createElement('button');
    const br = document.createElement('br');

    button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.appendChild(br);
    element.appendChild(button);

    // 이 예에서 print 모듈은 다운로드되지만 평가되지 않습니다.
    // 따라서 버튼을 클릭한 후에는 네트워크 요청이 발생하지 않습니다.
    button.onclick = e => {
+     const print = require('./print');
+     //            ^ 여기서 모듈을 평가합니다.
      const print = module.default;
-     //                  ^ 여기서 모듈을 평가합니다.

      print();
    };

    return element;
  }

  getComponent().then(component => {
    document.body.appendChild(component);
  });
  document.body.appendChild(component());

Using import.defer() with Context Modules

5.105.0+

import.defer()는 컨텍스트 모듈에서도 작동하며, 임포트 경로는 동적 표현식일 수 있습니다. Webpack은 일치하는 모든 모듈을 모듈 그래프에 포함시키지만, 선택된 모듈의 평가는 네임스페이스 객체의 속성에 처음 접근할 때까지 지연됩니다.

다음 예제는 동적 컨텍스트 경로를 사용하여 로케일 모듈의 평가를 지연시키는 방법을 보여줍니다.

src/locales/en.js

export const greeting = "Hello";

src/locales/fr.js

export const greeting = "Bonjour";

src/index.js

const language = navigator.language.split("-")[0]; // "en", "fr", etc.
const locale = import.defer("./locales/" + language + ".js");

document.getElementById("btn").addEventListener("click", () => {
  // The locale module is evaluated here, on first property access.
  document.getElementById("output").textContent = locale.greeting;
});

Webpack은 일치하는 모든 로케일 모듈을 실행 준비 상태로 만들지만, 선택된 모듈은 locale.greeting에 처음 접근할 때만 평가됩니다. 이를 통해 여러 로케일 파일을 즉시 실행하지 않고도 로드할 수 있습니다.

Frameworks

많은 프레임워크와 라이브러리에는 방법론 안에서 구현하는 방법에 대한 자체 권고안이 있습니다. 다음은 몇 가지 예시입니다.

ECMAScript Modules

ECMAScript 모듈(ESM)은 웹에서 모듈을 사용하기 위한 사양입니다. 모든 최신 브라우저와 권장하는 웹 모듈 코드 작성법에서 지원됩니다.

Webpack은 ECMAScript 모듈을 최적화하기 위한 처리를 지원합니다.

Exporting

export 키워드를 사용하면 ESM 항목을 다른 모듈에 노출할 수 있습니다.

export const CONSTANT = 42;

export let variable = 42;
// 오직 읽는 값만 노출됩니다.
// 외부에서 변수를 수정할 수 없습니다.

export function fun() {
  console.log("fun");
}

export class C extends Super {
  method() {
    console.log("method");
  }
}

let a, b, other;
export { a, b, other as c };

export default 1 + 2 + 3 + more();

Importing

import 키워드를 사용하면 다른 모듈에 대한 참조를 ESM으로 가져올 수 있습니다.

// 다른 모듈에 export하기 위해 "bindings"를 가져옵니다.
// 바인딩은 활성상태입니다. 값은 복사되지 않습니다.
// 대신 "변수"에 접근하면 현재 값을 얻습니다.
// 가져온 모듈에서
import { CONSTANT, variable } from "./module.js";

// "기본" export를 가져오는 단축입니다.
import theDefaultValue from "./module.js";

// 모든 export를 가지는 "네임스페이스 객체"를 가져옵니다.
import * as module from "./module.js";

module.fun();

When importing a namespace object from an ECMAScript Module, webpack follows the ESM convention of setting Symbol.toStringTag to "Module" on the namespace object.

Flagging modules as ESM

기본적으로 webpack은 파일이 ESM인지 또는 다른 모듈 시스템인지 자동으로 감지합니다.

Node.js는 package.json의 속성을 사용하여 파일의 모듈 유형을 명시적으로 설정하는 방법을 확립했습니다. package.json에서 "type": "module"을 설정하면 package.json 아래의 모든 파일이 ECMAScript 모듈이 됩니다. "type": "commonjs"를 설정하면 CommonJS 모듈이 됩니다.

{
  "type": "module"
}

또한, 파일은 .mjs 또는 .cjs 확장자를 사용해서 모듈 유형을 설정할 수 있습니다. .mjs는 ESM이 되도록 강제하고, .cjs는 CommonJS가 되도록 강제합니다.

DataURIs에서 text/javascript 또는 application/javascript mime 유형을 사용하면 모듈 유형도 ESM으로 강제로 적용됩니다.

모듈 형식뿐 아니라 모듈 플래그를 ESM으로 지정하는 것은 로직 해석, interop 로직 및 모듈에서 사용 가능한 심볼에 영향을 줍니다.

import.meta in ESM

Webpack은 ESM에서 사용할 수 있는 여러 import.meta 속성을 제공합니다.

속성설명
import.meta.url현재 모듈 파일의 URL로, new Worker()new URL()에 사용할 수 있습니다.
import.meta.webpackwebpack 메이저 버전 번호입니다. (예: 5)
import.meta.webpackHotmodule.hot와 동일한 기능으로, ESM에서 HMR에 사용할 수 있습니다.
import.meta.webpackContextrequire.context의 ESM 대응 API

예시 - 에셋에 import.meta.url 사용하기:

// 현재 모듈을 기준으로 같은 디렉터리의 파일 경로를 해석합니다.
const iconUrl = new URL("./icon.png", import.meta.url);
const img = document.createElement("img");
img.src = iconUrl.href;

예시 - ESM에서 HMR 사용하기:

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept("./module.js", () => {
    // 업데이트를 처리합니다.
  });
}

Top-Level Await

ESM에서는 모듈 최상위 레벨에서 await를 사용할 수 있습니다. Webpack은 해당 모듈을 자동으로 비동기 모듈로 처리합니다. 이 기능은 5.83.0부터 기본 활성화되었고, experiments.topLevelAwait 옵션 자체는 5.102.0에서 제거되었습니다. 즉, 별도 설정 없이 동작합니다.

// user.js (비동기 ESM 모듈)
const response = await fetch("/api/user");

export const user = await response.json();
// index.js - 비동기 모듈 import도 기대한 대로 동작합니다.
import { user } from "./user.js";

console.log(user.name);

Fully Specified Imports

ESM의 import는 더 엄격하게 해석됩니다. 파일이 ESM으로 지정된 경우, Node.js 규칙에 따라 상대 경로 요청에는 파일 확장자(예: *.js, *.mjs)를 포함해야 합니다.

// 실패합니다. 확장자가 없습니다.
import { helper as missingExt } from "./utils";

// ESM에서 올바른 방식입니다.
import { helper } from "./utils.js";

대규모 CJS 코드베이스를 마이그레이션할 때처럼 이 검사를 비활성화해야 한다면 fullySpecified=false를 사용할 수 있습니다.

// webpack.config.js
export default {
  module: {
    rules: [
      {
        test: /\.m?js/,
        resolve: {
          fullySpecified: false,
        },
      },
    ],
  },
};

CommonJS Interop

ESM에서는 require, module, exports, __filename, __dirname 같은 CommonJS 구문을 사용할 수 없습니다.

ESM에서 CommonJS 모듈을 import할 때는 default export만 사용할 수 있으며, 이는 module.exports 객체 전체를 가리킵니다.

// esm-consumer.js (ESM)
import cjs from "./cjs-module.js";
// CJS에서는 named import가 동작하지 않습니다.
import { foo } from "./cjs-module.js"; // undefined

// cjs-module.js (CommonJS)
module.exports = { foo: 1, bar: 2 };

console.log(cjs.foo); // 동작합니다. cjs는 exports 객체 전체입니다.

이 엄격한 동작은 webpack이 import된 모듈을 CommonJS로 판단할 때 적용됩니다. 해당 모듈이 자체적으로 ESM export 구문을 사용하면 webpack이 이를 자동으로 ESM으로 감지하므로 named import도 정상적으로 동작합니다. 이 문제는 "type": "module"이 설정된 프로젝트에서 .js 파일을 혼용할 때 자주 나타납니다. 프로젝트 내부 일부 파일은 ESM으로 처리되는 반면, node_modules의 서드파티 패키지는 여전히 CommonJS인 경우가 많기 때문입니다.

Common Migration Errors

ReferenceError: require is not defined

파일이 ESM으로 처리되면 CommonJS 전역 객체인 require, module, exports, __filename, __dirname를 사용할 수 없습니다.

해결: require()import 구문으로 바꾸세요. 조건부 또는 동적 로딩이 필요하다면 import()를 사용하세요.


Must use import to load ES Module (Node.js) / SyntaxError: Cannot use import statement in a module (browser)

이 오류는 ESM import/export 구문을 사용하는 파일이 ESM으로 지정되지 않았을 때 발생합니다. package.json"type": "module"이 없거나, 파일 확장자가 .mjs가 아니라 .js인 경우가 대표적입니다.

해결: package.json"type": "module"을 추가하거나 파일 확장자를 .mjs로 변경하세요.


Module not found: Error: Can't resolve './utils' (missing extension)

ESM에서는 상대 import에 파일 확장자를 포함해야 합니다. Webpack도 여기서는 Node.js의 ESM 규칙을 따릅니다.

해결: import { helper } from './utils'import { helper } from './utils.js'로 바꾸거나, 마이그레이션하는 동안에는 webpack 설정에서 fullySpecified: false를 지정해 이 검사를 끌 수 있습니다.

Shimming

webpack 컴파일러는 ES2015 모듈, CommonJS 또는 AMD로 작성된 모듈을 이해할 수 있습니다. 그러나 일부 써드 파티 라이브러리는 전역 종속성을 필요로 할 수 있습니다. (예: jQuery의 경우 $) 라이브러리는 내보낼 필요가 있는 전역 변수를 만들 수도 있습니다. 이러한 "깨진 모듈은" shimming이 작동하는 하나의 인스턴스입니다.

shimming 이 유용한 또 다른 경우는 더 많은 사용자를 지원하기 위해 브라우저 기능을 폴리필하려는 경우입니다. 이 경우 패치가 필요한 브라우저에만 해당 폴리필을 제공할 수 있습니다. (예: 요청 시 로드)

해당 글에서는 이러한 두 가지 사용 사례를 모두 살펴봅니다.

Shimming Globals

전역 변수 shimming의 첫 번째 사용 사례부터 시작하겠습니다. 시작하기 전에 프로젝트를 다시 한번 살펴보겠습니다.

프로젝트

webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 │   └── index.html
 ├── /src
 │   └── index.js
 └── /node_modules

우리가 사용했던 lodash 패키지를 기억하시나요? 데모 목적으로 애플리케이션에서 전역적으로 제공하고 싶다고 가정해 보겠습니다. 이를 위해 ProvidePlugin을 사용할 수 있습니다.

ProvidePlugin은 webpack을 통해 컴파일된 모든 모듈에서 패키지를 변수로 사용할 수 있게 해줍니다. 변수가 사용되는 것을 webpack에서 확인하면 최종 번들에 주어진 패키지를 포함합니다. lodash에 대한 import문을 제거하고 플러그인을 통해 제공해보겠습니다.

src/index.js

-import _ from 'lodash';
-
 function component() {
   const element = document.createElement('div');

-  // 이제 이 스크립트로 Lodash를 가져옵니다.
   element.innerHTML = _.join(['Hello', 'webpack'], ' ');

   return element;
 }

 document.body.appendChild(component());

webpack.config.js

 import path from "node:path";
 import { fileURLToPath } from 'url';
+import webpack from "webpack";



const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  plugins: [
+    new webpack.ProvidePlugin({
+      _: 'lodash',
+    }),
+  ],
 };

여기서 우리가 실질적으로 한 것은 webpack에게 알려주는 것입니다.

변수 _의 인스턴스가 하나 이상 존재한다면 lodash 패키지를 포함하고 필요한 모듈에 제공합니다.

빌드를 실행해도 동일한 출력이 표시되어야 합니다.

$ npm run build

..

[webpack-cli] Compilation finished
asset main.js 69.1 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 344 bytes 2 modules
cacheable modules 530 KiB
  ./src/index.js 191 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.x.x compiled successfully in 2910 ms

또한 ProvidePlugin에서 "배열 경로"(예: [module, child, ...children?])를 구성하여 모듈의 일부분만 내보낼 수 있습니다. 호출될 때마다 lodash에서 join 메소드만 제공하고 싶다고 가정해 보겠습니다.

src/index.js

 function component() {
   const element = document.createElement('div');

-  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  element.innerHTML = join(['Hello', 'webpack'], ' ');

   return element;
 }

 document.body.appendChild(component());

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
   plugins: [
     new webpack.ProvidePlugin({
-      _: 'lodash',
+      join: ['lodash', 'join'],
     }),
   ],
 };

lodash 라이브러리의 나머지는 삭제되므로 트리 쉐이킹이 잘 수행됩니다.

Granular Shimming

일부 레거시 모듈은 thiswindow 객체에 의존합니다. index.js를 업데이트해 보겠습니다.

 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

+  // `window의` 컨텍스트에 있다고 가정합니다.
+  this.alert("Hmmm, this probably isn't a great idea...");
+
   return element;
 }

 document.body.appendChild(component());

이것은 thismodule.exports와 같은 CommonJS 컨텍스트에서 모듈이 실행될 때 문제가 됩니다. 이 경우 imports-loader를 사용하여 this를 재정의할 수 있습니다.

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
 import { fileURLToPath } from 'url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 exports default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  module: {
+    rules: [
+      {
+        test: import.meta.resolve('./src/index.js'),
+        use: 'imports-loader?wrapper=window',
+      },
+    ],
+  },
   plugins: [
     new webpack.ProvidePlugin({
       join: ['lodash', 'join'],
     }),
   ],
 };

Global Exports

라이브러리가 사용자가 사용할 것으로 예상하는 전역 변수를 생성한다고 가정해 보겠습니다. 이를 증명하기 위해 작은 모듈을 추가할 수 있습니다.

프로젝트

  webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
+ │   ├── globals.js
  └── /node_modules

src/globals.js

const file = "blah.txt";
const helpers = {
  test() {
    console.log("test something");
  },
  parse() {
    console.log("parse something");
  },
};

소스 코드에서 이러한 작업을 수행할 수는 없지만, 위에 표시된 코드와 유사한 오래된 라이브러리를 접했을 수 있습니다. 이 경우 exports-loader를 사용하여 해당 전역 변수를 일반 모듈로 내보낼 수 있습니다. 예를 들어 filefile로, helpers.parseparse로 내보내 봅시다.

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
 import { fileURLToPath } from 'url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: import.meta.resolve('./src/index.js'),
         use: 'imports-loader?wrapper=window',
       },
+      {
+        test: import.meta.resolve('./src/globals.js'),
+        use:
+          'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+      },
     ],
   },
   plugins: [
     new webpack.ProvidePlugin({
       join: ['lodash', 'join'],
     }),
   ],
 };

이제 엔트리 스크립트(예: src/index.js)에서 const {file, parse} = require('./globals.js');를 사용할 수 있으며 원활하게 작동합니다.

Loading Polyfills

지금까지 논의한 대부분은 레거시 패키지 처리와 관련이 있습니다. 두 번째 주제인 폴리필로 넘어가겠습니다.

폴리필을 로드하는 방법에는 여러 가지가 있습니다. 예를 들어 babel-polyfill을 포함하려면 다음과 같이하면 됩니다.

npm install --save babel-polyfill

메인 번들에 포함되도록 import 합니다.

src/index.js

+import 'babel-polyfill';
+
 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

   // `window`의 컨텍스트에 있다고 가정합니다.
   this.alert("Hmmm, this probably isn't a great idea...");

   return element;
 }

 document.body.appendChild(component());

이 접근 방식은 번들 크기보다 정확성을 우선시합니다. 안전과 견고함을 위해서는 폴리필이나 shim이 다른 모든 코드보다 먼저 실행되어야 하므로 동기식으로 로드하거나 모든 앱 코드는 모든 폴리필이나 shim이 로드된 후에 로드해야 합니다. 또한 커뮤니티에는 최신 브라우저에 폴리필이 "필요하지 않다"거나 폴리필이나 shim이 누락된 기능을 추가하는 역할만 한다는 오해가 많이 있습니다. 사실, 가장 최신 브라우저에서도 종종 깨진 구현을 복구 합니다. 따라서 번들 크기 비용이 발생하더라도 모든 폴리필이나 shim을 무조건 동기식으로 로드하는 것이 모범 사례입니다.

문제가 해결됐다고 생각하고 위험을 감수하고 싶다면 다음과 같은 방법도 있습니다. import를 새 파일로 이동하고 whatwg-fetch 폴리필을 추가해 보겠습니다.

npm install --save whatwg-fetch

src/index.js

-import 'babel-polyfill';
-
 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

   // `window`의 컨텍스트에 있다고 가정합니다.
   this.alert("Hmmm, this probably isn't a great idea...");

   return element;
 }

 document.body.appendChild(component());

project

  webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
  │   ├── globals.js
+ │   └── polyfills.js
  └── /node_modules

src/polyfills.js

import "babel-polyfill";
import "whatwg-fetch";

webpack.config.js

 import path from "node:path";
 import webpack from "webpack";
 import { fileURLToPath } from 'url';

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

 export default {
-  entry: './src/index.js',
+  entry: {
+    polyfills: './src/polyfills',
+    index: './src/index.js',
+  },
   output: {
-    filename: 'main.js',
+    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: fileURLToPath(import.meta.resolve('./src/index.js')),
         use: 'imports-loader?wrapper=window',
       },
       {
         test: fileURLToPath(import.meta.resolve('./src/globals.js')),
         use:
           'exports-loader?type=commonjs&exports[]=file&exports[]=multiple|helpers.parse|parse',
       },
     ],
   },
   plugins: [
     new webpack.ProvidePlugin({
       join: ['lodash', 'join'],
     }),
   ],
 };

이를 통해 새로운 polyfills.bundle.js 파일을 조건부로 로드하는 로직을 추가 할 수 있습니다. 이 결정을 내리는 방법은 지원 기술과 브라우저에 따라 다릅니다. polyfill이 필요한지 여부를 확인하기 위해 몇 가지 간단한 테스트를 수행합니다.

dist/index.html

 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8" />
     <title>Getting Started</title>
+    <script>
+      const modernBrowser = 'fetch' in window && 'assign' in Object;
+
+      if (!modernBrowser) {
+        const scriptElement = document.createElement('script');
+
+        scriptElement.async = false;
+        scriptElement.src = '/polyfills.bundle.js';
+        document.head.appendChild(scriptElement);
+      }
+    </script>
   </head>
   <body>
-    <script src="main.js"></script>
+    <script src="index.bundle.js"></script>
   </body>
 </html>

이제 엔트리 스크립트에서 일부 데이터를 가져올 수 있습니다.

src/index.js

 function component() {
   const element = document.createElement('div');

   element.innerHTML = join(['Hello', 'webpack'], ' ');

   // `window`의 컨텍스트에 있다고 가정합니다.
   this.alert("Hmmm, this probably isn't a great idea...");

   return element;
 }

 document.body.appendChild(component());
+
+fetch('https://jsonplaceholder.typicode.com/users')
+  .then((response) => response.json())
+  .then((json) => {
+    console.log(
+      "We retrieved some data! AND we're confident it will work on a variety of browser distributions."
+    );
+    console.log(json);
+  })
+  .catch((error) =>
+    console.error('Something went wrong when fetching this data: ', error)
+  );

빌드를 실행하면 polyfills.bundle.js 파일이 생성되고 브라우저에서 원활하게 동작하게 됩니다. 이 설정은 개선될 수 있지만 실제로 필요한 사용자에게만 폴리필을 제공하는 방법에 대한 좋은 아이디어입니다.

Further Optimizations

babel-preset-env 패키지는 browserslist를 사용하여 브라우저 매트릭스에서 지원되지 않는 항목만 트랜스파일합니다. 이 사전 설정은 useBuiltIns 옵션(기본값 false)과 함께 제공되며, 전역 babel-polyfill을 가져오는 것을 import 패턴을 통해 더 세분화 된 기능으로 변환할 수 있습니다.

import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
import "core-js/modules/web.timers";
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";

자세한 내용은 babel-preset-env 문서를 참고하세요.

Node Built-Ins

process와 같은 Node 내장 기능은 특별한 로더나 플러그인을 사용하지 않고도 설정 파일에서 직접 폴리필 할 수 있습니다. 자세한 내용과 예제는 node 설정 페이지를 참고하세요.

Other Utilities

레거시 모듈을 다룰 때 도움이 될 수 있는 몇 가지 도구가 있습니다.

모듈의 AMD/CommonJS 버전이 없고 dist를 포함시키려는 경우, /configuration/module/#modulenoparse에 해당 모듈을 noParse로 지정할 수 있습니다. 이렇게 하면 webpack은 모듈을 파싱하거나 importrequire() 문을 해석하지 않고 포함합니다. 이 방법은 빌드 성능을 향상시키는 데에도 사용됩니다.

마지막으로 여러 모듈 스타일을 지원하는 모듈이 있습니다. (예: AMD, CommonJS 및 레거시의 조합) 대부분의 경우, 먼저 define을 확인한 다음 일부 코드를 사용하여 속성을 내보냅니다. 이 경우 imports-loader를 통해 additionalCode=var%define%20=%20false;를 설정하여 CommonJS 경로를 강제하는 것이 도움이 될 수 있습니다.

TypeScript

TypeScript는 일반 JavaScript로 컴파일되고 타입이 있는 상위 집합입니다. 이 가이드에서는 TypeScript를 webpack과 통합하는 방법에 대해 알아보겠습니다.

Basic Setup

먼저 다음을 실행하여 TypeScript 컴파일러와 로더를 설치하세요.

npm install --save-dev typescript ts-loader

이제 디렉터리 구조와 설정 파일을 수정합니다.

project

 webpack-demo
  ├── package.json
  ├── package-lock.json
+ ├── tsconfig.json
- ├── webpack.config.js
+ ├── webpack.config.ts
  ├── /dist
  │   ├── bundle.js
  │   └── index.html
  ├── /src
- │   ├── index.js
+ │   └── index.ts
  └── /node_modules

tsconfig.json

JSX를 지원하도록 간단하게 설정하고 TypeScript를 ES5로 컴파일 합니다.

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "esnext",
    "jsx": "react-jsx",
    "allowJs": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

tsconfig.json 설정 옵션에 대한 자세한 내용은 TypeScript 문서를 참고하세요.

webpack 설정에 대한 자세한 내용은 설정 콘셉트를 참고하세요.

이제 TypeScript를 처리하도록 webpack을 설정해 보겠습니다.

먼저 필요한 종속성을 설치하세요.

npm install --save-dev ts-node @types/node

webpack.config.ts

import path from "node:path";
import { fileURLToPath } from "url";
import webpack from "webpack";

// `devServer`를 구성하는 동안 TypeScript 오류가 발생하는 경우
import "webpack-dev-server";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const config: webpack.Configuration = {
  entry: "./src/index.ts",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};

export default config;

TypeScript 파일에 설정을 작성하는 방법에 대한 자세한 내용은 다음 링크를 참조하세요. (https://webpack.js.org/configuration/configuration-languages/#typescript)

이렇게하면 webpack이 ./index.ts 를 통해 진입하고, ts-loader를 통해 모든 .ts.tsx 파일을 로드합니다. 그리고 현재 디렉터리에 bundle.js파일을 출력합니다.

다음으로, ./index.ts에서 lodash를 가져오는 방식을 수정해야 합니다. lodash 정의에는 기본 export가 포함되어 있지 않으므로 import 문을 업데이트해야 합니다. 먼저 TypeScript 정의를 설치했는지 확인하세요.

npm install --save-dev @types/lodash

그런 다음 파일 맨 위에 있는 import 내용을 업데이트하세요.

./index.ts

- import _ from 'lodash';
+ import * as _ from 'lodash';

  function component() {
    const element = document.createElement('div');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

Ways to Use TypeScript in webpack.config.ts

webpack.config.ts에서 TypeScript를 사용하는 방법은 세 가지입니다.

  1. Node.js의 내장 타입 제거 기능을 사용하는 webpack (권장)

    webpack -c ./webpack.config.ts

    Node.js의 내장 타입 제거를 사용하여 구성을 로드하려고 시도한 다음, interpretrechoir를 사용하여 구성 파일을 로드하려고 시도합니다(이 경우 tsx 또는 ts-node 또는 기타 도구를 설치해야 합니다).

  2. Node.js에서 사용자 지정 --import/--require 사용:

    NODE_OPTIONS='--import=tsx --no-experimental-strip-types'  webpack -c ./webpack.config.ts
    NODE_OPTIONS='--require=ts-node/register --no-experimental-strip-types'  webpack -c ./webpack.config.ts

    Node.js 버전 22.7.0부터는 --no-experimental-strip-types 플래그가 필수입니다.

  3. Node.js 22.7.0 이상 버전에서 Node.js의 내장 변환 유형 기능을 사용하는 방법:

    열거형 선언, 매개변수 속성 등과 같이 JavaScript 코드 생성이 필요한, 지울 수 없는 TypeScript 구문의 변환을 가능하게 합니다.

    NODE_OPTIONS='--experimental-transform-types' webpack --disable-interpret -c ./webpack.config.ts

TypeScript Path Aliases

5.105.0+

tsconfig.json 파일에서 compilerOptions.paths 또는 compilerOptions.baseUrl을 사용하여 임포트 별칭을 생성하는 경우, webpack 5.105 버전부터는 webpack이 resolve.tsconfig 파일을 통해 이러한 별칭을 직접 읽을 수 있습니다. 따라서 기존의 tsconfig-paths-webpack-plugin 플러그인은 더 이상 사용하지 않아야 합니다.

resolve.tsconfigboolean | string | object 값을 허용합니다.

webpack.config.ts

export default {
  resolve: {
    tsconfig: true, // tsconfig.json 파일을 자동으로 찾습니다.
  },
};

특정 파일을 가리키는 문자열을 전달합니다(모노레포 환경에서 유용).

export default {
  resolve: {
    tsconfig: "./tsconfig.app.json",
  },
};

TypeScript 프로젝트 참조를 해결하기 위해 객체를 전달하세요(https://www.typescriptlang.org/docs/handbook/project-references.html).

export default {
  resolve: {
    tsconfig: {
      configFile: "./tsconfig.json",
      references: "auto", // tsconfig에서 참조를 상속받거나 경로 배열을 전달합니다.
    },
  },
};

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

위와 같이 하면 @/components/Button은 추가 플러그인이나 resolve.alias에 별칭을 중복 지정할 필요 없이 src/components/Button으로 해석됩니다.

Migrating from tsconfig-paths-webpack-plugin

현재 tsconfig-paths-webpack-plugin을 사용하고 있다면, 내장된 resolve.tsconfig 옵션으로 대체할 수 있습니다.

- 'tsconfig-paths-webpack-plugin'에서 TsconfigPathsPlugin을 가져옵니다.

  export default {
    resolve: {
-     plugins: [new TsconfigPathsPlugin()],
+     // 프로젝트 루트에서 tsconfig.json 파일을 자동으로 찾습니다.
+     tsconfig: true,
+
+     // 또는 명시적으로 하나를 가리키십시오.
+     // tsconfig: './tsconfig.app.json'
    },
  };

그런 다음 프로젝트에서 해당 패키지를 제거할 수 있습니다.

npm uninstall tsconfig-paths-webpack-plugin

Loader

ts-loader

이 가이드에서는 ts-loader를 사용하여 다른 웹 애셋 import 같은 추가적인 webpack 기능을 조금 더 쉽게 활성화 할 수 있습니다.

이미 babel-loader 사용하여 코드를 트랜스파일 하는 경우라면 @babel/preset-typescript를 사용하여 Babel이 추가 로더를 사용하는 대신 JavaScript와 TypeScript 파일을 모두 처리하도록 합니다. ts-loader와 달리, 기본 @babel/plugin-transform-typescript 플러그인은 어떠한 타입 검사도 수행하지 않습니다.

Source Maps

소스맵에 대한 자세한 내용은 개발 가이드를 참고하세요.

소스맵을 사용하려면 TypeScript가 컴파일된 JavaScript 파일로 인라인 소스맵을 출력하도록 설정해야 합니다. TypeScript 설정에 다음 내용을 꼭 추가해야합니다.

tsconfig.json

  {
    "compilerOptions": {
      "outDir": "./dist/",
+     "sourceMap": true,
      "noImplicitAny": true,
      "module": "esnext",
      "moduleResolution": "bundler",
      "target": "esnext",
      "jsx": "react-jsx",
      "allowJs": true,
  },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
  }

이제 webpack에 이러한 소스맵을 추출해 최종 번들에 포함되도록 지시해야 합니다.

webpack.config.ts

 import path from "node:path";
 import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

  export default {
    entry: './src/index.ts',
+   devtool: 'inline-source-map',
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          use: 'ts-loader',
          exclude: /node_modules/,
        },
      ],
    },
    resolve: {
      extensions: [ '.tsx', '.ts', '.js' ],
    },
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };

자세한 내용은 개발자 도구 문서를 참고하세요.

Client types

TypeScript 코드에서 import.meta.webpack과 같은 webpack 관련 기능을 사용할 수 있습니다. 그리고 webpack은 이에 대한 타입도 제공합니다. TypeScript reference 지시문을 추가하여 선언하면 됩니다.

/// <reference types="webpack/module" />
console.log(import.meta.webpack); // 위에서 선언된 참조가 없으면 TypeScript에서 에러가 발생합니다.

프로젝트 전체에 타입 지정을 활성화하려면 tsconfig.json 파일의 compilerOptions.typeswebpack/module을 추가하세요.

  {
    "compilerOptions": {
      "types": [
+       "webpack/module"
      ]
    }
  }

Using Third Party Libraries

npm으로부터 타사 라이브러리를 설치할 때는 해당 라이브러리에 대한 타입 정의를 설치해야 한다는 사실을 기억해야 합니다.

예를 들어 lodash를 사용하려면 다음 명령을 실행하여 해당 타입 정의를 설치해야 합니다.

npm install --save-dev @types/lodash

npm 패키지가 이미 패키지 번들에 선언 유형을 포함하고 있는 경우 해당 @types 패키지를 다운로드할 필요가 없습니다. 자세한 내용은 TypeScript 변경 로그 블로그를 참고하세요.

Importing Other Assets

TypeScript와 함께 비코드 애셋을 사용하려면 이러한 import에 대한 타입을 연기해야 합니다. 이를 위해서 프로젝트에 TypeScript에 대한 사용자 정의를 나타내는 custom.d.ts 파일이 필요합니다. .svg 파일에 대한 선언을 설정해 보겠습니다.

custom.d.ts

declare module "*.svg" {
  const content: any;
  export default content;
}

여기에서는 .svg로 끝나는 import를 지정하고 모듈의 contentany로 정의하여 SVG를 위한 새로운 모듈을 선언합니다. 타입을 문자열로 정의하여 URL이라는 것을 더 명확하게 할 수 있습니다. CSS, SCSS, JSON 등을 포함한 다른 애셋에도 동일한 개념이 적용됩니다.

Build Performance

빌드 도구에 대한 빌드 성능 가이드를 참고하세요.

Web Workers

webpack 5부터는, worker-loader 없이 Web Workers를 사용할 수 있습니다.

Syntax

new Worker(new URL("./worker.js", import.meta.url));
// 또는 매직 코멘트로 청크 이름을 사용자 정의하세요.
// https://webpack.js.org/api/module-methods/#magic-comments 내용을 참고하세요.
new Worker(
  /* webpackChunkName: "foo-worker" */ new URL("./worker.js", import.meta.url),
);

이 구문은 번들러 없이 코드를 실행할 수 있도록 선택되었으며, 브라우저의 기본 ECMAScript 모듈에서도 사용할 수 있습니다.

Worker API에서는 Worker 생성자가 스크립트의 URL을 나타내는 문자열을 허용한다고 설명하지만, webpack 5에서는 URL만 사용할 수 있다는 점에 유의하세요.

5.105.0+

new Worker()를 사용할 때, Webpack은 패키지의 exports 필드에 정의된 내보내기 조건 이름을 통해 워커 모듈을 찾을 수 있습니다. 이를 통해 패키지는 워커별 버전의 모듈을 자동으로 제공할 수 있습니다.

package.json (종속성 패키지에서):

{
  "name": "my-package",
  "exports": {
    ".": {
      "worker": "./index.worker.js",
      "default": "./index.js"
    }
  }
}

워커 컨텍스트 내에서 이 패키지를 임포트할 때:

// Inside a worker file
import { someFunction } from "my-package";

Webpack은 모듈을 워커 컨텍스트에서 사용할 경우 별도의 설정 없이 자동으로 index.worker.js로 해석합니다.

Example

src/index.js

const worker = new Worker(new URL("./deep-thought.js", import.meta.url));
worker.postMessage({
  question:
    "The Answer to the Ultimate Question of Life, The Universe, and Everything.",
});
worker.onmessage = ({ data: { answer } }) => {
  console.log(answer);
};

src/deep-thought.js

globalThis.onmessage = ({ data: { question } }) => {
  self.postMessage({
    answer: 42,
  });
};

Set a public path from a variable

변수에서 __webpack_public_path__를 설정하고 publicPathauto로 설정하면, 워커 청크는 별도의 런타임을 갖게 되고, Webpack 런타임은 publicPath를 자동으로 계산된 공개 경로로 설정합니다. 이는 예상과 다를 수 있습니다.

이 문제를 해결하려면 워커 코드 내에서 __webpack_public_path__를 설정해야 합니다. 예시는 다음과 같습니다.

worker.js

globalThis.onmessage = ({ data: { publicPath, ...otherData } }) => {
  if (publicPath) {
    __webpack_public_path__ = publicPath;
  }

  // 나머지 워커 코드
};

app.js

const worker = new Worker(new URL("./worker.js", import.meta.url));

worker.postMessage({ publicPath: globalThis.__MY_GLOBAL_PUBLIC_PATH_VAR__ });

When to use this:

이 패턴은 워커가 추가 청크를 로드해야 하고 자산 기본 URL이 런타임에 결정되는 경우(예: CDN 또는 다중 도메인 배포 사용 시)에만 필요합니다.

워커는 격리된 전역 범위에서 실행되므로 자동으로 감지된 공개 경로가 메인 스레드에서 사용하는 경로와 다를 수 있습니다. 이러한 경우 공개 경로(__webpack_public_path__)를 워커에 명시적으로 전달하고 워커 런타임 내에서 설정해야 합니다.

참고: 이는 고급 사용 사례입니다. 워커가 추가 청크를 로드하지 않거나 자산이 정적 동일 출처 경로에서 제공되는 경우 일반적으로 __webpack_public_path__를 수동으로 설정할 필요가 없습니다.

Node.js

이 섹션에서는 worker_threads 모듈을 통해 Node.js 환경에서 웹 워커를 사용하는 방법을 설명합니다.

비슷한 구문이 12.17.0 이상의 Node.js에서 지원됩니다.

import { Worker } from "node:worker_threads";

new Worker(new URL("./worker.js", import.meta.url));

이 구문은 ESM에서만 사용할 수 있습니다. CommonJS 구문의 Worker는 webpack 이나 Node.js 모두 지원되지 않습니다.

Progressive Web Application

프로그레시브 웹 애플리케이션(또는 PWA)은 네이티브 애플리케이션과 유사한 경험을 제공하는 웹 앱입니다. PWA에 기여할 수 있는 많은 것들이 있습니다. 이 중에서 가장 중요한 것은 오프라인 일 때 앱이 작동할 수 있는 기능입니다. 이는 Service Workers라는 웹 기술을 사용하여 이루어집니다.

이 섹션에서는 앱에 오프라인 경험을 추가하는 데 중점을 둡니다. 웹 앱에 대한 오프라인 지원을 보다 쉽게 설정하는 데 도움이 될 도구를 제공하는 Workbox라는 Google 프로젝트를 사용하여 이 작업을 수행합니다.

We Don't Work Offline Now

지금까지 로컬 파일 시스템으로 직접 이동하여 출력을 확인했습니다. 일반적으로 실제 사용자는 네트워크를 통해 웹 앱에 접근합니다. 브라우저는 .html, .js, 그리고 .css 파일같은 필요한 애셋을 제공할 서버와 통신합니다.

간단한 서버를 사용하여 테스트해 보겠습니다. npm install http-server --save-dev 커맨드로 http-server 패키지를 설치하여 사용해 보겠습니다. 또한 package.jsonscripts 섹션을 수정하여 start 스크립트를 추가하겠습니다.

package.json

{
  ...
  "scripts": {
-    "build": "webpack"
+    "build": "webpack",
+    "start": "http-server dist"
  },
  ...
}

참고: webpack DevServer는 기본적으로 인-메모리를 사용합니다. http-server가 ./dist 디렉터리 파일을 제공하도록 하려면 devserverdevmiddleware.writeToDisk 옵션을 활성화해야 합니다.

npm run build 커맨드를 실행하여 프로젝트를 빌드합니다. 그런 다음 npm start 커맨드를 실행합니다. 그러면 다음과 같이 출력됩니다.

> http-server dist

Starting up http-server, serving dist
Available on:
  http://xx.x.x.x:8080
  http://127.0.0.1:8080
  http://xxx.xxx.x.x:8080
Hit CTRL-C to stop the server

만약 브라우저를 http://localhost:8080로 연다면 dist 디렉터리에서 제공되는 webpack 애플리케이션을 볼 수 있습니다. 서버를 중지하고 새로 고침하면 webpack 애플리케이션을 더 이상 사용할 수 없습니다.

이것이 변경하고자 하는 것입니다. 이 문서의 끝에서는 이제 서버를 중지하고, 새로 고침을 눌러도 애플리케이션을 계속 볼 수 있습니다.

Adding Workbox

Workbox webpack 플러그인을 추가하고 webpack.config.js파일을 수정해 보겠습니다.

npm install workbox-webpack-plugin --save-dev

webpack.config.js

  import path from "node:path";
  import { fileURLToPath } from 'node:url';
  import HtmlWebpackPlugin from "html-webpack-plugin";
+ import WorkboxPlugin from "workbox-webpack-plugin";

 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);

  export default {
    entry: {
      app: './src/index.js',
      print: './src/print.js',
    },
    plugins: [
      new HtmlWebpackPlugin({
-       title: 'Output Management',
+       title: 'Progressive Web Application',
      }),
+     new WorkboxPlugin.GenerateSW({
+       // 이 옵션은 ServiceWorkers가 빠르게 도달하도록 장려합니다
+       // 그리고 "오래된" SW가 돌아다니는 것을 허용하지 않습니다
+       clientsClaim: true,
+       skipWaiting: true,
+     }),
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true,
    },
  };

이제 npm run build를 수행할 때 어떤 일이 발생하는지 살펴보겠습니다.

...
                  Asset       Size  Chunks                    Chunk Names
          app.bundle.js     545 kB    0, 1  [emitted]  [big]  app
        print.bundle.js    2.74 kB       1  [emitted]         print
             index.html  254 bytes          [emitted]
precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js  268 bytes          [emitted]
      service-worker.js       1 kB          [emitted]
...

보다시피 service-worker.jsprecache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js라는 2개의 추가 파일이 생성됩니다. service-worker.js는 서비스 워커 파일이고 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.jsservice-worker.js가 실행되기 위해 필요한 파일입니다. 사용자가 생성한 파일은 다를 수 있습니다. 하지만 service-worker.js 파일은 있어야 합니다.

이제 서비스 워커를 만들었습니다. 다음 단계는 무엇일까요?

Registering Our Service Worker

서비스 워커를 등록하여 실행 할 수 있도록 합시다. 아래의 등록 코드를 추가하면 됩니다.

index.js

  import _ from 'lodash';
  import printMe from './print.js';

+ if ('serviceWorker' in navigator) {
+   window.addEventListener('load', () => {
+     navigator.serviceWorker.register('/service-worker.js').then(registration => {
+       console.log('SW registered: ', registration);
+     }).catch(registrationError => {
+       console.log('SW registration failed: ', registrationError);
+     });
+   });
+ }

한 번 더 npm run build를 통해 등록 코드를 포함한 앱 버전을 빌드합니다. 그런 다음 npm start를 실행합니다. http://localhost:8080로 이동하여 콘솔을 살펴보세요. 어딘가에 다음 내용이 표시됩니다.

SW registered

이제 테스트해 보겠습니다. 서버를 중지하고 페이지를 새로 고침 합니다. 브라우저가 서비스 워커를 지원하는 경우 애플리케이션을 계속해서 확인할 수 있습니다. 하지만 서비스 워커가 서비스를 제공하는 것이지 서버가 제공하는 것은 아닙니다.

Conclusion

Workbox 프로젝트를 사용하여 오프라인 앱을 빌드했습니다. 웹 앱을 PWA로 전환하는 여정을 시작했습니다. 이제 더 나아가는 것에 대해 생각할 수 있습니다. 도움이 되는 유용한 리소스는 여기에서 찾을 수 있습니다.

Public Path

publicPath 설정은 다양한 경우에서 유용하게 사용될 수 있습니다. 애플리케이션의 모든 애셋에 대한 기본 경로를 지정할 수 있습니다.

Use Cases

이 기능이 특히 유용한 실제 애플리케이션에서의 몇 가지 사용 사례가 있습니다. 기본적으로 output.path 디렉터리로 내보내는 모든 파일은 output.publicPath에서 참조됩니다. 여기에는 하위 청크 (코드 스플리팅을 통해 생성됨) 및 디펜던시 그래프의 일부 애셋(예: 이미지, 글꼴 등)이 포함됩니다.

Environment Based

예를 들어 개발 과정에서 index 페이지와 동일한 수준에 있는 assets/ 폴더가 있을 수 있습니다. 프로덕션 환경에서 정적 애셋을 CDN에 호스팅하려면 어떻게 해야할까요?

이 문제를 해결하기 위해 오랫동안 사용 중인 환경 변수를 사용해봅시다. ASSET_PATH 변수가 있다고 가정해 보겠습니다.

import webpack from "webpack";

// 환경 변수를 사용하고 존재하지 않는다면 루트를 사용하세요.
const ASSET_PATH = process.env.ASSET_PATH || "/";

export default {
  output: {
    publicPath: ASSET_PATH,
  },

  plugins: [
    // 코드에서 환경 변수를 안전하게 사용할 수 있습니다.
    new webpack.DefinePlugin({
      "process.env.ASSET_PATH": JSON.stringify(ASSET_PATH),
    }),
  ],
};

On The Fly

또 다른 사용 사례는 publicPath를 직접 설정하는 것입니다. Webpack은 이를 가능하게 하는 __webpack_public_path라는__ 전역 변수를 노출합니다. 따라서 애플리케이션의 엔트리 포인트에서 간단하게 처리할 수 있습니다.

__webpack_public_path__ = process.env.ASSET_PATH;

이게 전부입니다. 이미 설정에서 DefinePlugin을 사용하고 있으므로 process.env.ASSET_PATH는 항상 정의되어 안전하게 사용할 수 있습니다.

// entry.js
import "./public-path";
import "./app";

Automatic publicPath

publicPath가 무엇인지 미리 알 수 없는 경우가 있는데, Webpack은 import.meta.url, document.currentScript, script.src 또는 self.location과 같은 변수를 통해 publicPath를 자동으로 파악하여 처리해 줍니다. 이때 필요한 것은 output.publicPathauto로 설정하는 것입니다.

webpack.config.js

export default {
  output: {
    publicPath: "auto",
  },
};

document.currentScript가 지원되지 않는 경우(예: IE 브라우저)에는 currentScript Polyfill과 같은 폴리필을 포함해야 합니다.

Integrations

일반적인 오해를 푸는 것부터 시작하겠습니다. Webpack은 BrowserifyBrunch 같은 모듈 번들러 입니다. MakeGrunt, Gulp와 같은 태스크 러너가 아닙니다. 태스크 러너는 프로젝트의 린트, 빌드, 테스트와 같은 일반적인 태스크의 자동화를 처리합니다. 번들러와 비교하면 태스크 러너는 더 높은 수준에 집중합니다. 번들링의 문제는 webpack에 맡겨두고 더 높은 수준의 툴링에 대한 이점을 가질 수 있습니다.

번들러는 JavaScript와 스타일시트를 배포할 수 있도록 준비하고, 브라우저에 적합한 형식으로 변환하는 데 도움을 줍니다. 예를 들어 JavaScript를 minify하거나 청크로 분리하고 lazy-load하여 성능을 개선할 수 있습니다. 번들링은 웹 개발에서 가장 중요한 과제 중 하나이며, 이를 잘 해결하면 전체 과정의 많은 어려움을 덜어낼 수 있습니다.

좋은 소식은 올바른 방법으로 접근하면 약간 중복이 있더라도 태스크 러너와 번들러를 함께 잘 사용할 수 있습니다. 이 가이드는 널리 사용되는 태스크 러너와 webpack을 어떻게 통합하는지에 대해 이해하기 쉽게 설명합니다.

NPM Scripts

webpack 사용자는 npm scripts를 태스크 러너로 종종 사용합니다. 이것은 좋은 시작점입니다. 크로스 플랫폼(Cross-platform) 지원이 문제가 될 수 있지만, 여기엔 여러 가지 해결 방법이 있습니다. 대부분의 사용자는 아니지만 많은 사용자가 간단한 npm scripts와 다양한 수준의 webpack 설정 및 툴링을 사용할 수 있습니다.

따라서 webpack의 핵심은 번들링에 초점을 맞추고 있지만, 태스크 러너의 일반적인 작업을 webpack으로 수행할 수 있도록 하는 다양한 확장 기능이 있습니다. 별도의 도구를 통합하면 복잡성이 늘어나기 때문에 시작하기 전에 장단점을 고려해야 합니다.

Grunt

Grunt를 사용한다면 grunt-webpack 패키지를 사용하는 것이 좋습니다. grunt-webpack을 사용하면 webpack이나 webpack-dev-server를 태스크로 실행할 수 있으며, template tags 내에서 통계에 접근 할 수 있고, 개발과 프로덕션의 설정을 분리하는 등의 작업을 수행할 수 있습니다. 설치하지 않았다면 grunt-webpackwebpack의 설치를 시작하세요.

npm install --save-dev grunt-webpack webpack

그리고 설정을 등록하고 태스크를 로드합니다.

Gruntfile.js

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

module.exports = function (grunt) {
  grunt.initConfig({
    webpack: {
      options: {
        stats: !process.env.NODE_ENV || process.env.NODE_ENV === "development",
      },
      prod: webpackConfig,
      dev: { watch: true, ...webpackConfig },
    },
  });

  grunt.loadNpmTasks("grunt-webpack");
};

더 자세한 정보를 원하시면 저장소를 방문해 보세요.

Gulp

Gulp 역시 webpack-stream 패키지를 통해 매우 간단하게 통합할 수 있습니다. (a.k.a. gulp-webpack) 이 경우 webpackwebpack-stream에 직접적인 의존성이 있으므로 별도로 설치할 필요가 없습니다.

npm install --save-dev webpack-stream

webpack 대신 require('webpack-stream')을 사용하고 선택적으로 설정을 전달합니다.

gulpfile.js

import gulp from "gulp";
import webpack from "webpack-stream";

gulp.task("default", () =>
  gulp
    .src("src/entry.js")
    .pipe(
      webpack({
        // 모든 설정 옵션...
      }),
    )
    .pipe(gulp.dest("dist/")),
);

자세한 내용은 webpack-stem 저장소를 참고하세요.

Mocha

mocha-webpack 유틸리티는 webpack을 Mocha와 깔끔하게 통합해줍니다. 저장소는 장단점에 대한 자세한 내용을 제공하지만 본질적으로 mocha-webpack은 Mocha 자체와 거의 동일한 CLI를 제공하고 향상된 watch 모드와 경로 분석과 같은 다양한 webpack 기능을 제공하는 간단한 래퍼입니다. 다음은 설치 및 테스트 스위트를 실행하는 방법에 대한 간단한 예입니다. (./test에 있음).

npm install --save-dev webpack mocha mocha-webpack
mocha-webpack 'test/**/*.js'

자세한 내용은 mocha-webpack 저장소를 참고하세요.

Karma

karma-webpack 패키지를 사용하면 webpack을 사용하여 Karma에서 파일을 전처리할 수 있습니다.

npm install --save-dev webpack karma karma-webpack

karma.conf.js

export default function (config) {
  config.set({
    frameworks: ["webpack"],
    files: [
      { pattern: "test/*_test.js", watched: false },
      { pattern: "test/**/*_test.js", watched: false },
    ],
    preprocessors: {
      "test/*_test.js": ["webpack"],
      "test/**/*_test.js": ["webpack"],
    },
    webpack: {
      // 모든 커스텀 webpack 설정...
    },
    plugins: ["karma-webpack"],
  });
}

자세한 내용은 karma-webpack 저장소를 참고하세요.

Advanced entry

Multiple file types per entry

JavaScript의 스타일에 import를 사용하지 않는 애플리케이션(싱글 페이지 애플리케이션 혹은 다른 이유로인해)에서 CSS 및 JavaScript와 기타 파일에 대해 각각 별도의 번들을 얻기 위해 엔트리에 값 배열을 사용하여 다른 유형의 파일을 제공할 수 있습니다.

예를 들어 보겠습니다. 홈과 계정을 위한 두 가지 페이지 유형이 있는 PHP 애플리케이션이 있습니다. 홈 페이지는 다른 레이아웃을 갖고 있고, 나머지 애플리케이션(계정 페이지)과는 공유할 수 없는 JavaScript가 있습니다. 홈 페이지를 위해 애플리케이션 파일에서 home.jshome.css를 출력하고, 계정 페이지를 위해 account.jsaccount.css를 출력하려고 합니다.

home.js

console.log("home page type");

home.scss

// 홈 페이지의 개별 스타일

account.js

console.log("account page type");

account.scss

// 계정 페이지의 개별 스타일

CSS를 위한 프로덕션 모드에서 MiniCssExtractPlugin을 모범사례로 사용하겠습니다.

webpack.config.js

import MiniCssExtractPlugin from "mini-css-extract-plugin";

export default {
  mode: process.env.NODE_ENV,
  entry: {
    home: ["./home.js", "./home.scss"],
    account: ["./account.js", "./account.scss"],
  },
  output: {
    filename: "[name].js",
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          // 개발환경에서는 style-loader로 대체 합니다
          process.env.NODE_ENV !== "production"
            ? "style-loader"
            : MiniCssExtractPlugin.loader,
          "css-loader",
          "sass-loader",
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
  ],
};

위의 구성으로 webpack을 실행하면 다른 출력경로를 지정하지 않았기 때문에 ./dist로 출력됩니다. ./dist 디렉터리는 이제 4개의 파일이 포함됩니다.

  • home.js
  • home.css
  • account.js
  • account.css

Asset Modules

애셋 모듈은 로더를 추가로 구성하지 않아도 애셋 파일(폰트, 아이콘 등)을 사용할 수 있습니다.

webpack 5 이전에는 아래의 로더를 사용하는 것이 일반적이었습니다.

  • raw-loader 파일을 문자열로 가져올 때
  • url-loader 파일을 data URI 형식으로 번들에 인라인 추가 할 때
  • file-loader 파일을 출력 디렉터리로 내보낼 때

이러한 로더를 대체하기 위해서 애셋 모듈에는 5개의 새로운 모듈 유형이 추가되었습니다.

  • asset/resource는 별도의 파일을 내보내고 URL을 추출합니다. 이전에는 file-loader를 사용하여 처리할 수 있었습니다.
  • asset/inline은 애셋의 data URI를 내보냅니다. 이전에는 url-loader를 사용하여 처리할 수 있었습니다.
  • asset/source는 애셋의 소스 코드를 내보냅니다. 이전에는raw-loader를 사용하여 처리할 수 있었습니다.
  • asset/bytes는 애셋의 Uint8Array 뷰를 내보냅니다.
  • asset은 data URI와 별도의 파일 내보내기 중에서 자동으로 선택합니다. 이전에는 애셋 크기 제한이 있는 url-loader를 사용했습니다.

webpack 5의 애셋 모듈과 함께 이전 애셋 로더(예 :file-loader/url-loader/raw-loader)를 사용할 때 애셋 모듈이 애셋을 중복으로 처리하지 않도록 할 수 있습니다. 이는 애셋의 모듈 유형을 'javascript/auto'로 설정하여 적용 가능합니다.

webpack.config.js

export default {
  module: {
   rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            }
          },
        ],
+       type: 'javascript/auto'
      },
   ]
  },
}

애셋 로더의 새로운 URL 호출에서 발생한 애셋을 제외하려면 로더 설정에 dependency : {not: ['url']}을 추가합니다.

webpack.config.js

export default {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
+       dependency: { not: ['url'] },
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },
    ],
  }
}

Public Path

기본적으로 asset 유형은 __webpack_public_path__ + import.meta를 수행합니다. 즉, 설정에서 output.publicPath를 설정하면 asset이 로드되는 URL을 재정의할 수 있습니다.

On The Fly Override

코드에서 __webpack_public_path__를 설정한 경우 asset 로딩 로직을 깨지지 않도록 설정하려면 함수를 사용하지 않고 앱의 첫 번째 코드로 실행해야 합니다. 예시로는 내용이 포함된 publicPath.js라는 파일을 갖는 경우입니다.

__webpack_public_path__ = "https://cdn.url.com";

그런 다음 webpack.config.js에서 entry 필드를 다음과 같이 업데이트합니다.

export default {
  entry: ["./publicPath.js", "./App.js"],
};

또는 webpack 설정을 수정하지 않고 App.js에서 다음을 수행할 수 있습니다. 유일한 단점은 여기에서 순서를 강제해야 하고, 일부 린팅 도구와 충돌할 수 있다는 것입니다.

import "./publicPath.js";

Resource type

webpack.config.js

import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
+ module: {
+   rules: [
+     {
+       test: /\.png/,
+       type: 'asset/resource',
+     },
+   ],
+ },
};

src/index.js

import mainImage from "./images/main.png";

img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'

모든 .png 파일을 출력 디렉터리로 내보내고 해당 경로를 번들에 삽입합니다. 게다가 outputPathpublicPath를 사용자 지정할 수 있습니다.

Custom output filename

파일을 출력 디렉터리로 내보낼 때 asset/resource 모듈은 기본적으로 [hash][ext][query] 파일명을 사용합니다.

webpack 설정에서 output.assetModuleFilename을 설정하여 이 템플릿을 수정할 수 있습니다.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
+   assetModuleFilename: 'images/[hash][ext][query]',
  },
  module: {
    rules: [
      {
        test: /\.png/,
        type: 'asset/resource',
      },
    ],
  },
};

특정 디렉터리에 애셋을 내보낼때 출력 파일명을 사용자 정의하는 경우도 있습니다.

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
+   assetModuleFilename: 'images/[hash][ext][query]',
  },
  module: {
    rules: [
+     {
+       test: /\.html/,
+       type: 'asset/resource',
+       generator: {
+         filename: 'static/[hash][ext][query]',
+       },
+     },
    ],
  },
};

이 설정을 통해 모든 html 파일을 출력 디렉터리 내의 static 디렉터리로 내보내게 됩니다.

Rule.generator.filenameoutput.assetModuleFilename과 같으며 assetasset/resource 모듈에서만 동작합니다.

Inlining assets

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
+     {
+       test: /\.svg/,
+       type: 'asset/inline',
+     },
    ],
  },
};

src/index.js

import metroMap from "./images/metro.svg";

block.style.background = `url(${metroMap})`; // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)

모든 .svg 파일은 data URI로 번들에 삽입됩니다.

Custom data URI generator

기본적으로 webpack에서 내보낸 data URI는 Base64 알고리즘을 사용하여 인코딩된 파일 콘텐츠를 의미합니다.

커스텀 인코딩 알고리즘을 사용하려면, 파일 콘텐츠 인코딩을 위한 커스텀 함수를 지정해야 합니다.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';
+ import svgToMiniDataURI from "mini-svg-data-uri";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);


export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.svg/,
        type: 'asset/inline',
+       generator: {
+         dataUrl: content => {
+           content = content.toString();
+           return svgToMiniDataURI(content);
+         },
+       },
      },
    ],
  },
};

이제 모든 .svg 파일이 mini-svg-data-uri 패키지를 통해 인코딩됩니다.

Source type

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
+     {
+       test: /\.txt/,
+       type: 'asset/source',
+     },
    ],
  },
};

src/example.txt

Hello world

src/index.js

import exampleText from "./example.txt";

block.textContent = exampleText; // 'Hello world';

Alternative usage:

src/index.js

import exampleText from "./example.txt" with { type: "text" };

block.textContent = exampleText; // 'Hello world';

모든 .txt 파일은 UTF-8 문자열로 번들에 포함됩니다.

URL assets

new URL('./path/to/asset', import.meta.url)을 사용할 때 webpack은 애셋 모듈도 함께 생성합니다.

src/index.js

const logo = new URL("./logo.svg", import.meta.url);

설정의 target에 따라 webpack은 위 코드를 다른 결과로 컴파일합니다.

// target: web
new URL(
  `${__webpack_public_path__}logo.svg`,
  document.baseURI || self.location.href,
);

// target: webworker
new URL(`${__webpack_public_path__}logo.svg`, self.location);

// target: node, node-webkit, nwjs, electron-main, electron-renderer, electron-preload, async-node
new URL(
  `${__webpack_public_path__}logo.svg`,
  require("node:url").pathToFileUrl(__filename),
);
// ECMA 모듈 출력이 활성화된 경우 모든 대상
new URL(`${__webpack_public_path__}logo.svg`, import.meta.url);

webpack 5.38.0 버전부터 new URL()에서도 데이터 URL이 지원됩니다.

src/index.js

const url = new URL("data:,", import.meta.url);
console.log(url.href === "data:,");
console.log(url.protocol === "data:");
console.log(url.pathname === ",");

Asset type

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
+     {
+       test: /\.txt/,
+       type: 'asset',
+     },
    ],
  },
};

이제 webpack은 기본 조건에 따라서 resourceinline 중에서 자동으로 선택합니다. 크기가 8kb 미만인 파일은 inline 모듈로 처리되고 그렇지 않으면 resource 모듈로 처리됩니다.

webpack 설정의 module rule 단계에서 Rule.parser.dataUrlCondition.maxSize 옵션을 설정하여 이 조건을 변경할 수 있습니다.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.txt/,
        type: 'asset',
+       parser: {
+         dataUrlCondition: {
+           maxSize: 4 * 1024, // 4kb
+         },
+       },
      },
    ],
  },
};

또한 함수를 지정하여 모듈의 인라인 여부를 결정할 수 있습니다.

Bytes type

webpack.config.js

import path from "node:path";
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
+     {
+       test: /\.txt/,
+       type: 'asset/bytes',
+     },
    ],
  },
};

src/example.txt

Hello world

src/index.js

import exampleText from "./example.txt";

const decoder = new TextDecoder("utf-8");
const textString = decoder.decode(exampleText);

block.textContent = textString; // 'Hello world';

다른 사용법:

src/index.js

import exampleText from "./example.txt" with { type: "bytes" };

const decoder = new TextDecoder("utf-8");
const textString = decoder.decode(exampleText);

block.textContent = textString; // 'Hello world';

모든 .txt 파일은 텍스트 인코딩이나 변환 없이 원시 바이트(Uint8Array) 형태로 번들에 삽입됩니다.

Replacing Inline Loader Syntax

Asset Modules 및 Webpack 5 이전에는, 위에 언급한 레거시 로더와 함께 inline syntax를 사용할 수 있었습니다.

현재는 모든 인라인 로더 구문을 제거하고 resourceQuery 조건을 사용하여 인라인 구문의 기능을 모방하는 것이 좋습니다.

예를 들어, raw-loaderasset/source 유형으로 바꾸는 경우입니다.

- import myModule from 'raw-loader!my-module';
+ import myModule from 'my-module?raw';

webpack 설정입니다.

module: {
    rules: [
    // ...
+     {
+       resourceQuery: /raw/,
+       type: 'asset/source',
+     }
    ]
  },

원시 애샛을 다른 로더에서 처리하지 못하도록 제외하려면, 부정적 조건을 사용하십시오.

module: {
    rules: [
    // ...
+     {
+       test: /\.m?js$/,
+       resourceQuery: { not: [/raw/] },
+       use: [ ... ]
+     },
      {
        resourceQuery: /raw/,
        type: 'asset/source',
      }
    ]
  },

또는 oneOf 규칙 목록입니다. 여기에서는 첫 번째로 일치하는 규칙만 적용됩니다.

module: {
    rules: [
    // ...
+     { oneOf: [
        {
          resourceQuery: /raw/,
          type: 'asset/source',
        },
+       {
+         test: /\.m?js$/,
+         use: [ ... ]
+       },
+     ] }
    ]
  },

Built-in query suffixes

5.109.0+

experiments.futureDefaults가 활성화되면 webpack이 위 rule을 대신 등록합니다. Vite의 asset query와 유사하게 ?raw, ?url, ?inline, ?no-inline query suffix가 모든 import에서 별도 설정 없이 동작합니다.

import source from "./file.txt?raw"; // asset/source - 원시 파일 content를 string으로 가져옵니다.
import dataUri from "./icon.svg?inline"; // asset/inline - data: URI
import url from "./image.png?url"; // asset/resource - emit된 파일 URL
import fileUrl from "./small.png?no-inline"; // asset/resource - rule이 inline하더라도 절대 inline하지 않습니다.

suffix는 query string 어디에서든 매치됩니다(?foo&raw도 동작). oneOf 목록으로 처리되므로 처음 일치한 suffix만 우선합니다. 일반 default rule이기 때문에 직접 작성한 module.rules가 우선하며 이를 override할 수 있습니다.

Disable emitting assets

서버 사이드 랜더링과 같은 사용 사례의 경우 애셋 방출을 비활성화할 수 있습니다. 이는 Rule.generator 하위의 emit 옵션을 통해 설정 가능합니다.

export default {
  // …
  module: {
    rules: [
      {
        test: /\.png$/i,
        type: "asset/resource",
        generator: {
          emit: false,
        },
      },
    ],
  },
};

Package exports

import "package" 또는 import "package/sub/path"와 같이 모듈을 요청할 때, 패키지의 package.jsonexports 필드에 어떤 모듈을 사용할지 선언할 수 있습니다. 이를 통해 main 필드 응답을 반환하는 기본 구현을 대체합니다. index.js 파일은 "package"를, 파일 시스템 조회는 "package/sub/path"를 대체합니다.

exports 필드가 명시되면 이러한 모듈 요청만 사용 가능합니다. 그 외 다른 요청은 ModuleNotFound 오류가 발생합니다.

General syntax

일반적으로 exports 필드는 객체를 가지며 객체의 각각의 프로퍼티에는 모듈 요청의 하위 경로가 명시되어 있어야 합니다. 위의 예시에서는 다음 프로퍼티를 사용할 수 있습니다. import "package"에는 "."을, import "package/sub/path"에는 "./sub/path"를 사용할 수 있습니다. /로 끝나는 프로퍼티는 요청에 이 접두사를 포함하여 이전 파일 시스템 조회 알고리즘으로 전달합니다. *로 끝나는 프로퍼티의 경우 *는 어떤 값이든 가질 수 있으며, 프로퍼티 값의 모든 *는 가져온 값으로 대체됩니다.

예제:

{
  "exports": {
    ".": "./main.js",
    "./sub/path": "./secondary.js",
    "./prefix/": "./directory/",
    "./prefix/deep/": "./other-directory/",
    "./other-prefix/*": "./yet-another/*/*.js"
  }
}
모듈 요청결과
package.../package/main.js
package/sub/path.../package/secondary.js
package/prefix/some/file.js.../package/directory/some/file.js
package/prefix/deep/file.js.../package/other-directory/file.js
package/other-prefix/deep/file.js.../package/yet-another/deep/file/deep/file.js
package/main.jsError

Alternatives

패키지 작성자는 하나의 결과 대신 여러 개의 결과를 제공할 수 있습니다. 이 경우 결과 목록을 순서대로 시도하고 첫 번째 유효한 결과를 사용합니다.

노트: 모든 유효한 결과가 아니라 첫 번째 유효한 결과만 사용합니다.

예제:

{
  "exports": {
    "./things/": ["./good-things/", "./bad-things/"]
  }
}

여기서 package/things/apple.../package/good-things/apple 또는 .../package/bad-things/apple에서 찾을 수 있습니다.

예를 들어, 다음과 같은 설정이 있습니다.

{
  "exports": {
    ".": ["-bad-specifier-", "./non-existent.js", "./existent.js"]
  }
}

Webpack 5.94.0+에서는 이전 동작이 existent.js로 확인되었을 것이지만 non-existent.js가 발견되지 않아 오류가 발생합니다.

Conditional syntax

exports 필드에 직접 결과를 제공하는 대신 패키지 작성자는 모듈 시스템이 환경 조건에 따라 결과를 선택하도록 할 수 있습니다.

이 경우 결과에 대한 객체 매핑 조건을 사용해야 합니다. 조건은 객체 순서대로 시도됩니다. 유효하지 않은 결과가 포함된 조건은 건너뜁니다. 논리적 AND를 만들기 위해 조건이 중첩될 수 있습니다. 객체의 마지막 조건은 특별한 "default" 조건일 수 있습니다. 이 조건은 항상 매치됩니다.

예제:

{
  "exports": {
    ".": {
      "red": "./stop.js",
      "yellow": "./stop.js",
      "green": {
        "free": "./drive.js",
        "default": "./wait.js"
      },
      "default": "./drive-carefully.js"
    }
  }
}

위 조건은 다음과 같이 번역됩니다.

if (red && valid("./stop.js")) return "./stop.js";
if (yellow && valid("./stop.js")) return "./stop.js";
if (green) {
  if (free && valid("./drive.js")) return "./drive.js";
  if (valid("./wait.js")) return "./wait.js";
}
if (valid("./drive-carefully.js")) return "./drive-carefully.js";
throw new ModuleNotFoundError();

사용 가능한 조건은 모듈 시스템 및 도구에 따라 다릅니다.

Abbreviation

패키지에 대한 단일 엔트리 (".")만 지원하는 경우 { ".": ...} 객체 중첩을 생략할 수 있습니다.

{
  "exports": "./index.mjs"
}
{
  "exports": {
    "red": "./stop.js",
    "green": "./drive.js"
  }
}

Notes about ordering

각 키가 조건인 객체일 경우 프로퍼티 순서가 매우 중요합니다. 조건은 명시된 순서대로 처리됩니다.

예: { "red": "./stop.js", "green": "./drive.js"} != {"green": "./drive.js", "red": "./stop.js"}(redgreen 조건이 모두 설정된 경우 첫 번째 프로퍼티가 사용됩니다)

각 키가 하위 경로인 객체에서는 프로퍼티(하위 경로) 순서가 크게 중요하지 않습니다. 덜 구체적인 경로보다 더 구체적인 경로가 우선됩니다.

예: { "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" } == { "./a/b/c": "./z", "./a/b/": "./y/", "./a/": "./x/" } (순서는 항상 ./a/b/c > ./a/b/ > ./a/ 입니다)

main, module, browser 또는 커스텀 필드와 같은 다른 패키지 엔트리 필드보다 exports 필드가 우선됩니다.

Support

기능지원
"." 속성Node.js, webpack, rollup, esinstall, wmr
일반 속성Node.js, webpack, rollup, esinstall, wmr
/로 끝나는 속성Node.js(1), webpack, rollup, esinstall(2), wmr(3)
*로 끝나는 속성Node.js, webpack, rollup, esinstall
alternativesNode.js, webpack, rollup, esinstall(4)
path에만 축약형 사용Node.js, webpack, rollup, esinstall, wmr
조건에만 축약형 사용Node.js, webpack, rollup, esinstall, wmr
조건 구문Node.js, webpack, rollup, esinstall, wmr
중첩된 조건 구문Node.js, webpack, rollup, wmr(5)
조건 순서Node.js, webpack, rollup, wmr(6)
"default" 조건Node.js, webpack, rollup, esinstall, wmr
경로 순서Node.js, webpack, rollup
매핑되지 않았을 때 오류Node.js, webpack, rollup, esinstall, wmr(7)
조건과 경로를 혼합해서 사용할 때 오류Node.js, webpack, rollup

(1) Node.js 17에서 제거되었습니다. 대신 *를 사용하세요.

(2) "./" 키는 의도적으로 무시됩니다.

(3) 프로퍼티 값은 무시되고 프로퍼티 키가 대상으로 사용됩니다. 키와 값이 동일한 경우에만 효과적으로 매핑을 허용합니다.

(4) 구문을 지원하지만, 항상 첫 번째 엔트리가 사용되므로 실제로는 사용할 수 없습니다.

(5) 다른 형제 부모 조건으로 폴백시 올바르지 않게 처리됩니다.

(6) require 조건의 경우 객체 순서가 올바르지 않게 처리됩니다. 이것은 의도적으로, wmr이 참조하는 구문과 다르지 않기 때문입니다.

(7) "exports": "./file.js" 축약형을 사용하는 경우 package/not-existing과 같은 모든 요청은 이에 맞게 해석됩니다. 축약형을 사용하지 않는 경우 package/file.js와 같이 직접 파일에 접근해도 오류로 이어지지 않습니다.

Conditions

Reference syntax

모듈을 참조하는 데 사용되는 구문에 따라 다음 조건 중 하나가 설정됩니다.

조건설명지원
importESM 또는 유사한 구문에서 요청이 발생합니다.Node.js, webpack, rollup, esinstall(1), wmr(1)
requireCommonJs/AMD 또는 유사한 구문에서 요청이 발생합니다.Node.js, webpack, rollup, esinstall(1), wmr(1)
style스타일시트 참조에서 요청이 발생합니다.-
sasssass 스타일시트 참조에서 요청이 발생합니다.-
asset애셋 참조에서 요청이 발생합니다.-
script모듈 시스템 없이 스크립트 태그를 사용할때 요청이 발생합니다.-

부가적으로 아래의 조건도 설정할 수 있습니다.

조건설명지원
modulejavascript를 참조 가능한 모든 모듈 구문은 ESM을 지원합니다.
(import 또는 require와 함께 사용했을 때)
webpack, rollup, wmr
esmodules지원하는 도구에서 항상 설정합니다.wmr
typestype 선언과 관련 있는 typescript로부터 요청이 발생합니다.-

(1) 참조 구문에서 importrequire는 모두 독립적으로 설정됩니다. require는 항상 더 낮은 우선순위를 갖습니다.

import

다음 구문은 import 조건을 설정합니다.

  • ESM의 ESM import 선언
  • JS import() 표현식
  • HTML의 HTML <script type="module">
  • HTML의 HTML <link rel="preload/prefetch">
  • JS new Worker(..., { type: "module" })
  • WASM import섹션
  • ESM HMR(webpack) import.hot.accept/decline([...])
  • JS Worklet.addModule
  • 자바스크립트를 엔트리 포인트로 사용

require

다음 구문은 require 조건을 설정합니다.

  • CommonJs require(...)
  • AMD define()
  • AMD require([...])
  • CommonJs require.resolve()
  • CommonJs (webpack) require.ensure([...])
  • CommonJs (webpack) require.context
  • CommonJs HMR (webpack) module.hot.accept/decline([...])
  • HTML <script src="...">

style

다음 구문은 style 조건을 설정합니다.

  • CSS @import
  • HTML <link rel="stylesheet">

asset

다음 구문은 asset 조건을 설정합니다.

  • CSS url()
  • ESM new URL(..., import.meta.url)
  • HTML <img src="...">

script

다음 구문은 script 조건을 설정합니다.

  • HTML <script src="...">

script는 모듈 시스템을 지원하지 않는 경우에만 설정해야 합니다. CommonJs를 지원하는 시스템에서 스크립트를 전처리하는 경우, require로 설정해야 합니다.

이 조건은 HTML 페이지에서 스트립트 태그로 삽입할 수 있고 추가 전처리가 없는 자바스크립트 파일을 찾을 때 사용해야 합니다.

Optimizations

다양한 최적화를 위해 다음 조건이 설정됩니다.

조건설명지원
production프로덕션 환경.
개발 도구를 포함하지 않아야 합니다.
webpack
development개발 환경.
개발 도구를 포함해야 합니다.
webpack

노트: productiondevelopment는 모두가 사용하는 것이 아닙니다. 이 중 아무것도 설정되지 않은 경우는 가정하지 않아야 합니다.

Target environment

대상 환경에 따라 다음 조건이 설정됩니다.

조건설명지원
browserCode will run in a browser.webpack, esinstall, wmr
electronCode will run in electron.(1)webpack
workerCode will run in a (Web)Worker.(1)webpack
workletCode will run in a Worklet.(1)-
nodeCode will run in Node.js.Node.js, webpack, wmr(2)
denoCode will run in Deno.-
react-nativeCode will run in react-native.-

(1) electron, workerworklet은 컨텍스트에 따라 node 또는 browser와 결합합니다.

(2) 브라우저 대상 환경에 대해 설정됩니다.

각 환경에는 여러 버전이 있으므로 다음 가이드라인이 적용됩니다.

  • node: 호환성은 engines 필드를 참고하세요.
  • browser: 패키지를 배포하는 시점의 현재 Spec 및 4단계 제안과 호환됩니다. 폴리필과 트랜스파일은 소비하는 쪽에서 처리되어야 합니다.
    • 폴리필이나 트랜스파일이 불가능한 기능은 사용이 제한되므로 주의하여 사용해야 합니다.
  • deno: TBD
  • react-native: TBD

Conditions: Preprocessor and runtimes

소스 코드를 전처리하는 도구에 따라 다음 조건이 설정됩니다.

조건설명지원
webpackwebpack을 통해 처리됩니다.webpack

아쉽지만 Node.js 런타임에 대한 node-js 조건이 없습니다. 이것은 Node.js에 대한 예외 처리를 단순화합니다.

Conditions: Custom

다음 도구는 커스텀 조건을 지원합니다.

도구지원노트
Node.js지원--conditions CLI 인자를 사용.
webpack지원resolve.conditionNames 설정 옵션을 사용.
rollup지원@rollup/plugin-node-resolve 에서 exportConditions 옵션을 사용.
esinstall미지원-
wmr미지원-

커스텀 조건에는 다음 네이밍 스키마를 권장합니다.

<company-name>:<condition-name>

예: example-corp:beta, google:internal, `

Common patterns

패키지의 모든 패턴은 단일 "." 엔트리로 해석되지만, 각 엔트리의 패턴을 반복하여 복수의 엔트리로 확장할 수도 있습니다.

이 패턴은 엄격한 규칙이 아닌 가이드로 사용해야 합니다. 개별 패키지에 맞게 조정할 수 있습니다.

이러한 패턴은 다음과 같은 목표와 가정을 기반으로 합니다.

  • 패키지가 운영되지 않는다.
    • 어떤 시점에서 패키지가 더 이상 운영되지 않는다고 가정합니다. 하지만 패키지는 계속 사용됩니다.
    • exports는 향후 알려지지 않은 케이스에 대한 폴백으로 작성되어야 합니다. 이를 위해 default 조건을 사용할 수 있습니다.
    • 미래는 알 수 없기 때문에 브라우저와 유사한 환경, ESM과 유사한 모듈 시스템이라고 가정합니다.
  • 모든 도구가 모든 조건을 지원하지 않는다.
    • 이러한 케이스를 처리하려면 폴백을 사용해야 합니다.
    • 일반적으로 다음과 같은 폴백이 합리적으로 보입니다.
      • ESM > CommonJs
      • Production > Development
      • 브라우저 > node.js

패키지의 의도에 따라 다른 방법이 알맞을 수 있으며 패턴은 이를 따라야 합니다. 예를 들면, 커맨드라인 도구의 경우 브라우저와 같은 미래 환경에 대한 폴백은 별로 의미가 없으며, 이 경우에는 node.js와 같은 환경 및 폴백을 대신 사용해야 합니다.

사용 케이스가 복잡할 경우 조건을 중첩하여 여러 패턴을 결합해야 합니다.

Target environment independent packages

이 패턴은 환경별 API를 사용하지 않는 패키지에 적합합니다.

Providing only an ESM version

{
  "type": "module",
  "exports": "./index.js"
}

노트: ESM만 제공하면 node.js에 대한 제한이 따릅니다. 이러한 패키지는 Node.js >= 14 에서 import를 사용할 때만 동작합니다. require()으로는 동작하지 않습니다.

Providing CommonJs and ESM version (stateless)

{
  "type": "module",
  "exports": {
    "node": {
      "module": "./index.js",
      "require": "./index.cjs"
    },
    "default": "./index.js"
  }
}

대부분의 도구는 ESM 버전을 받습니다. 하지만 Node.js는 예외입니다. require()를 사용할 때 CommonJs 버전을 얻습니다. require()import를 참조할 때 패키지의 두 인스턴스로 이어지지만, 패키지에 state가 없기 때문에 문제 되지 않습니다.

require() ESM을 지원하는 도구로 노드 대상 코드를 전처리할 때 module 조건은 최적화를 위해 사용됩니다. (예: Node.js 용 번들러) 이러한 도구의 경우 예외를 건너뜁니다. 기술적으로 선택 사항이지만 그렇지 않으면 번들러에는 패키지 소스 코드가 두 번 포함됩니다.

JSON 파일에서 패키지 state를 분리할 수 있는 경우 stateless 패턴을 사용할 수도 있습니다. JSON은 다른 모듈 시스템 그래프에 영향 없이 CommonJs 및 ESM에서 사용할 수 있습니다.

여기서 stateless는 클래스 인스턴스가 instanceof로 테스트 되지 않음을 의미합니다. 이중 모듈 인스턴스화로 인해 두 개의 다른 클래스가 있을 수 있기 때문입니다.

Providing CommonJs and ESM version (stateful)

{
  "type": "module",
  "exports": {
    "node": {
      "module": "./index.js",
      "import": "./wrapper.js",
      "require": "./index.cjs"
    },
    "default": "./index.js"
  }
}
// wrapper.js
import cjs from "./index.cjs";

export const A = cjs.A;
export const B = cjs.B;

stateful 패키지에서는 패키지가 두 번 인스턴스화되지 않도록 해야합니다.

대부분의 도구에서 문제가 되지 않지만 Node.js는 여기서도 예외입니다. Node.js는 항상 CommonJs 버전을 사용하고 ESM 래퍼를 사용하여 ESM에 명명된 export를 노출합니다.

다시 module 조건을 최적화를 위해 사용합니다.

Providing only a CommonJs version

{
  "type": "commonjs",
  "exports": "./index.js"
}

"type": "commonjs"를 제공하면 CommonJs 파일을 정적으로 감지할 수 있습니다.

Providing a bundled script version for direct browser consumption

{
  "type": "module",
  "exports": {
    "script": "./dist-bundle.js",
    "default": "./index.js"
  }
}

dist-bundle.js"type": "module".js를 사용하더라도 이 파일은 ESM 형식이 아닙니다. 스크립트 태그로 직접 사용 할 수 있도록 전역을 사용해야 합니다.

Providing devtools or production optimizations

이러한 패턴은 패키지에 개발용과 프로덕션용 두 가지 버전이 있을 때 의미가 있습니다. 예를 들면 개발 버전에는 더 나은 오류 메시지 또는 부가적인 경고를 위한 추가 코드가 포함될 수 있습니다.

Without Node.js runtime detection

{
  "type": "module",
  "exports": {
    "development": "./index-with-devtools.js",
    "default": "./index-optimized.js"
  }
}

development 조건을 지원하면 개발을 위해 향상된 버전을 사용합니다. 프로덕션 버전 또는 모드를 알 수 없는 경우에는 최적화된 버전을 사용합니다.

With Node.js runtime detection

{
  "type": "module",
  "exports": {
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "node": "./wrapper-process-env.cjs",
    "default": "./index-optimized.js"
  }
}

wrapper-process-env.cjs

module.exports =
  process.env.NODE_ENV !== "development"
    ? require("./index-optimized.cjs")
    : require("./index-with-devtools.cjs");

프로덕션/개발 모드를 감지할 때 production 또는 development 조건을 통한 정적 감지를 선호합니다.

Node.js는 런타임에 process.env.NODE_ENV를 통해 프로덕션/개발 모드를 감지할 수 있으므로 Node.js에서 이를 폴백으로 사용합니다. 동기화 조건부 import ESM은 불가능하며 패키지를 두 번 로드하지 않아야 하므로 CommonJs로 런타임을 감지해야 합니다.

모드를 감지할 수 없는 경우 프로덕션 버전으로 대체합니다.

Providing different versions depending on target environment

패키지가 향후 환경을 지원할 수 있도록 폴백 환경을 선택해야 합니다. 일반적으로 브라우저와 같은 환경을 가정해야 합니다.

Providing Node.js, WebWorker and browser versions

{
  "type": "module",
  "exports": {
    "node": "./index-node.js",
    "worker": "./index-worker.js",
    "default": "./index.js"
  }
}

Providing Node.js, browser and electron versions

{
  "type": "module",
  "exports": {
    "electron": {
      "node": "./index-electron-node.js",
      "default": "./index-electron.js"
    },
    "node": "./index-node.js",
    "default": "./index.js"
  }
}

Combining patterns

Example 1

아래 예제는 process.env에 대한 런타임 감지와 프로덕션 및 개발을 위해 최적화를 제공하는 패키지입니다. CommonJs 및 ESM 버전도 제공합니다.

{
  "type": "module",
  "exports": {
    "node": {
      "development": {
        "module": "./index-with-devtools.js",
        "import": "./wrapper-with-devtools.js",
        "require": "./index-with-devtools.cjs"
      },
      "production": {
        "module": "./index-optimized.js",
        "import": "./wrapper-optimized.js",
        "require": "./index-optimized.cjs"
      },
      "default": "./wrapper-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

Example 2

이 예제는 Node.js, 브라우저 및 electron을 지원합니다. process.env에 대한 런타임 감지와 프로덕션 및 개발을 위한 최적화를 제공하며 CommonJs 및 ESM 버전도 제공합니다.

{
  "type": "module",
  "exports": {
    "electron": {
      "node": {
        "development": {
          "module": "./index-electron-node-with-devtools.js",
          "import": "./wrapper-electron-node-with-devtools.js",
          "require": "./index-electron-node-with-devtools.cjs"
        },
        "production": {
          "module": "./index-electron-node-optimized.js",
          "import": "./wrapper-electron-node-optimized.js",
          "require": "./index-electron-node-optimized.cjs"
        },
        "default": "./wrapper-electron-node-process-env.cjs"
      },
      "development": "./index-electron-with-devtools.js",
      "production": "./index-electron-optimized.js",
      "default": "./index-electron-optimized.js"
    },
    "node": {
      "development": {
        "module": "./index-node-with-devtools.js",
        "import": "./wrapper-node-with-devtools.js",
        "require": "./index-node-with-devtools.cjs"
      },
      "production": {
        "module": "./index-node-optimized.js",
        "import": "./wrapper-node-optimized.js",
        "require": "./index-node-optimized.cjs"
      },
      "default": "./wrapper-node-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

맞습니다. 복잡해 보이죠. node에만 CommonJs 버전이 필요하고 process.env를 사용하여 프로덕션/개발 모드를 감지 할 수 있다고 가정하여 복잡성을 줄였습니다.

Guidelines

  • default export를 피하십시오. 툴링 마다 다르게 처리됩니다. 명명된 export만 사용하세요.
  • 다른 조건에 대해 다른 API 또는 의미를 부여하지 않아야 합니다.
  • 소스 코드를 ESM으로 작성하고 babel, typescript 또는 유사한 도구를 통해 CJS로 트랜스파일하세요.
  • package.json에서 .cjs 또는 type: "commonjs"를 사용하여 소스 코드를 CommonJs로 명확하게 표시하세요. CommonJs 또는 ESM을 사용하는 경우 도구가 이를 정적으로 감지 할 수 있습니다. 이는 ESM만 지원하고 CommonJs는 지원하지 않는 도구의 경우 중요합니다.
  • 패키지에서 사용하는 ESM은 다음 유형의 요청을 지원합니다.
    • package.json이 있는 다른 패키지를 가리키는 모듈 요청을 지원합니다.
    • 패키지 내의 다른 파일을 가리키는 상대적 요청을 지원합니다.
      • 패키지 외부의 파일을 가리켜서는 안 됩니다.
    • data: URL 요청을 지원합니다.
    • 기타 절대적 요청 또는 서버와 관련된 요청은 기본적으로 지원되지 않지만, 일부 도구 또는 환경에서는 지원할 수 있습니다.

Modern Web Platform

이 가이드는 Web Components, Import Maps, 그리고 Service Workers를 사용하는 Progressive Web Apps(PWA)를 위한 실용적인 webpack 패턴을 설명합니다. 각 섹션은 문제를 설명하고, 바로 복사해 사용할 수 있는 최소 설정을 보여주며, 앞으로의 webpack 개선과 비교했을 때 현재의 한계도 함께 짚어줍니다.

Web Components with webpack

Problem

둘 이상의 JavaScript 번들이 같은 태그 이름에 대해 customElements.define()를 실행하면, 브라우저는 DOMExceptionFailed to execute 'define' on 'CustomElementRegistry'를 발생시킵니다. 이런 일은 요소를 등록하는 모듈이 중복될 때 자주 발생합니다. 즉, 서로 다른 엔트리 포인트나 비동기 청크에 등록 코드 사본이 각각 들어 있어 두 번들이 같은 태그에 대해 모두 define을 실행하게 됩니다.

Approach

요소를 정의하는 모듈이 한 번만 로드되는 단일 공유 청크에 들어가도록 optimization.splitChunks를 사용하세요. cacheGroups를 조정해 요소 정의 코드(또는 src/elements/ 같은 전용 폴더)를 하나의 청크로 강제로 묶을 수 있습니다. 전반적인 아이디어는 Prevent Duplication을 참고하세요.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: {
    main: "./src/main.js",
    admin: "./src/admin.js",
  },
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        // 공유되는 custom element module을 하나의 async chunk에 넣습니다.
        customElements: {
          test: /[\\/]src[\\/]elements[\\/]/,
          name: "custom-elements",
          chunks: "all",
          enforce: true,
        },
      },
    },
  },
};

두 엔트리 모두 동일한 등록 모듈(예: ./elements/my-element.js)을 import하도록 해야 webpack이 중복 등록 코드를 mainadmin에 각각 인라인하는 대신, 하나의 custom-elements.js 청크를 생성할 수 있습니다.

Limitations and future work

청크 분할만으로 브라우저의 규칙이 바뀌지는 않습니다. 태그 이름은 여전히 문서마다 정확히 한 번만 등록되어야 합니다. Webpack은 아직 청크 그래프 제어를 넘어서는, “이 커스텀 요소를 한 번만 등록하라”는 일급 기능을 제공하지 않습니다. 빌드 전반에서 커스텀 요소 등록을 중복 제거하는 네이티브 지원은 계획되어 있지만, 그때까지는 공유 청크와 단일 등록 모듈에 의존해야 합니다.

Import Maps with webpack

Problem

Import maps는 브라우저가 bare specifier를 해석할 수 있게 해줍니다(importmap.json 또는 인라인 <script type="importmap">를 통한 import "lodash-es" 같은 형태). Webpack이 해당 의존성을 번들링한다면 그 의존성에 대해 import map은 필요하지 않습니다. 반대로 애플리케이션 코드는 bare import를 유지하면서 의존성은 브라우저가 URL(CDN 또는 /vendor/)에서 직접 로드하게 하고 싶다면, 그 모듈들을 externals로 지정해 webpack이 import map과 일치하는 import 구문을 출력하도록 해야 합니다.

Approach

ES module output(experiments.outputModuleoutput.module)을 활성화하고, 정적 import에 대해 externalsType: "module"을 설정한 뒤, 각 bare specifier를 externals에 import map에서 브라우저가 해석할 것과 같은 문자열로 나열하세요.

webpack.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  mode: "production",
  experiments: {
    outputModule: true,
  },
  entry: "./src/index.js",
  externalsType: "module",
  externals: {
    "lodash-es": "lodash-es",
  },
  output: {
    module: true,
    filename: "[name].mjs",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};

importmap.json (HTML과 함께 제공되어야 하며 URL은 실제 배포 환경과 일치해야 합니다)

로컬 vendor 파일:

{
  "imports": {
    "lodash-es": "/vendor/lodash-es.js"
  }
}

CDN(직접 호스팅 불필요):

{
  "imports": {
    "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4/+esm"
  }
}

"lodash-es" 키는 externals와 소스 코드 안의 specifier(import … from "lodash-es") 모두와 일치해야 합니다. 값은 브라우저가 로드할 URL이며, 로컬 경로일 수도 있고 CDN URL일 수도 있습니다. Webpack은 해당 파일의 유효성을 검사하지 않습니다.

index.html (순서가 중요합니다. 번들보다 import map이 먼저 와야 합니다)

<script type="importmap" src="/importmap.json"></script>
<script type="module" src="/dist/main.mjs"></script>

Limitations and future work

Webpack은 importmap.json을 대신 생성하거나 업데이트해주지 않습니다. specifier와 URL이 externals 및 서버 레이아웃과 계속 맞도록 이 맵을 직접 관리해야 합니다. 현재 webpack 5에서는 import map 자동 생성 기능을 사용할 수 없으며, 앞으로의 도구 개선으로 이 수작업 단계가 줄어들 수 있습니다.

Progressive Web Apps (PWA) and Service Workers

Problem

오래 유지되는 캐싱을 위해서는 HTML에는 안정적인 URL이 필요하고, 스크립트와 스타일에는 버전이 반영된 URL이 필요합니다. output.filename[contenthash]를 사용하면 이 URL은 빌드할 때마다 바뀝니다. service worker의 precache 목록은 매 빌드 후의 정확한 URL을 나열해야 하며, 그렇지 않으면 오프라인 셸이 존재하지 않는 파일을 가리키게 됩니다.

workbox-webpack-pluginGenerateSW 플러그인은 전체 service worker를 대신 생성해줍니다. 편리하긴 하지만, service worker 코드에 대한 완전한 제어(커스텀 라우팅, skipWaiting 동작, 또는 [contenthash] 및 다른 플러그인과의 연동)가 필요하다면 **InjectManifest**가 더 적합합니다. 워커 코드는 직접 작성하고, Workbox가 빌드 시점에 webpack의 에셋 목록을 바탕으로 precache manifest를 주입합니다.

Approach

출력되는 에셋에는 [contenthash]를 사용하고, workbox-webpack-plugin의 **InjectManifest**를 추가하세요. 소스 템플릿에서는 workbox-precaching을 import한 뒤 precacheAndRoute(self.__WB_MANIFEST)를 호출합니다. 그러면 플러그인이 self.__WB_MANIFEST를 webpack 에셋 목록(해시된 파일명 포함)으로 치환합니다.

설치:

npm install workbox-webpack-plugin workbox-precaching --save-dev

webpack.config.js

import path from "node:path";
import { fileURLToPath } from "node:url";
import HtmlWebpackPlugin from "html-webpack-plugin";
import { InjectManifest } from "workbox-webpack-plugin";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: "./src/index.js",
  output: {
    filename: "[name].[contenthash].js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({ title: "PWA + content hashes" }),
    new InjectManifest({
      swSrc: path.resolve(__dirname, "src/service-worker.js"),
      swDest: "service-worker.js",
    }),
  ],
};

src/service-worker.js (precache 템플릿)

import { precacheAndRoute } from "workbox-precaching";

// build 시 webpack의 precache manifest(해시된 asset URL)로 대체됩니다.
precacheAndRoute(globalThis.__WB_MANIFEST);

생성된 service-worker.js는 애플리케이션 쪽(예: src/index.js)에서 navigator.serviceWorker.register("/service-worker.js")로 등록하세요. 이 파일은 올바른 scope로 dist/에서 제공되어야 합니다.

Limitations and future work

출력 파일명과 플러그인 구성이 바뀌면 **InjectManifest**도 그에 맞게 계속 동기화해야 합니다. 커스텀 워커가 필요 없다면 여전히 GenerateSW가 더 단순한 선택입니다. Webpack은 내장 service worker precache 생성기를 제공하지 않으며, 해시된 에셋과의 더 긴밀한 통합은 앞으로의 릴리스에서 추가될 수 있습니다. 그전까지는 Workbox의 **InjectManifest**가 [contenthash] 출력과 precaching을 맞추는 가장 안정적인 방법입니다.

Native CSS

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

Getting Started

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

webpack.config.js

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

이 옵션을 활성화하면 webpack은 .css 파일을 일급 모듈로 이해하여 @importurl()을 파싱하고, 스타일시트를 추출하며, content hash를 생성하고, css-loader, style-loader, mini-css-extract-plugin 없이도 CSS Modules를 지원합니다.

Importing CSS

실험 기능을 활성화한 뒤에는 JavaScript에서 .css 파일을 직접 import할 수 있습니다.

src/index.js

import "./styles.css";

const element = document.createElement("h1");
element.textContent = "Hello native CSS";
document.body.appendChild(element);

src/styles.css

h1 {
  color: #1f6feb;
}

Webpack은 이 CSS를 처리해 빌드 결과물에 포함합니다.

CSS module types

Native CSS는 네 가지 Rule.type 값을 도입합니다. 어떤 값이 적용되는지 이해하는 것이 마이그레이션의 핵심인데, 각각이 서로 다른 css-loader modules.mode에 대응하기 때문입니다.

TypeScopingcss-loader equivalent
css전역, CSS Modules 파싱 없음modules: false
css/global전역 selector, 하지만 :local()은 적용됨modules.mode: 'global'
css/module기본적으로 local, :global()로 전역으로 빠져나갈 수 있음modules.mode: 'local'
css/auto*.module.css / *.modules.css에는 css/module, 그 외에는 css/global 선택modules.auto: true

webpack이 /\.css$/i에 대해 기본으로 추가하는 rule은 css/auto이므로, *.module.css 파일은 CSS Modules가 되고 나머지는 모두 전역으로 유지됩니다. 이는 가장 일반적인 css-loader 설정과 기본적으로 동일합니다.

CSS Modules

css/auto를 사용할 때는 파일 이름을 *.module.css(또는 *.modules.css)로 지정하면 CSS Modules로 처리할 수 있습니다.

src/button.module.css

.button {
  background: #0d6efd;
  color: white;
  border: 0;
  border-radius: 4px;
  padding: 8px 12px;
}

src/index.js

import * as styles from "./button.module.css";

const button = document.createElement("button");
button.className = styles.button;
button.textContent = "Click me";
document.body.appendChild(button);

parsergenerator 옵션으로 CSS Modules 동작을 사용자 정의할 수 있습니다. 아래의 All options with examples를 참고하세요.

webpack.config.js

export default {
  experiments: {
    css: true,
  },
  module: {
    parser: {
      "css/auto": {
        namedExports: true,
      },
    },
    generator: {
      "css/auto": {
        exportsConvention: "camel-case-only",
        localIdentName: "[uniqueName]-[id]-[local]",
      },
    },
  },
};

Supported CSS Modules features

Native CSS Modules는 css-loader와 같은 authoring 기능을 이해하므로, 대부분의 stylesheet는 수정 없이 그대로 마이그레이션할 수 있습니다.

  • composes: 하나의 local class를 다른 local class로 합성합니다(composes: foo from "./other.module.css" 포함). export 결과는 공백으로 구분된 class name 목록으로 해석됩니다.
  • @value: 재사용 가능한 값을 선언하고 import합니다(@value primary: #1f6feb;, @value primary from "./vars.module.css").
  • :export: 임의의 key/value 쌍을 JavaScript에 노출합니다.
  • :local() / :global(): 어떤 module type에서든 인라인으로 scope를 전환합니다.
/* button.module.css */
@value brand: #1f6feb;

.base {
  padding: 8px 12px;
}
.primary {
  composes: base;
  background: brand;
}
:export {
  brandColor: brand;
}

Output modes (exportType)

하나의 CSS module은 네 가지 방식으로 출력할 수 있습니다. exportType parser 옵션으로 이를 선택하며, 각각은 기존 툴체인의 다른 부분을 대체합니다.

exportTypeBehaviorReplaces
"link" (default).css 파일로 추출하고 <link>로 로드mini-css-extract-plugin
"style"런타임에서 <style> element를 주입style-loader
"text"CSS를 string으로 exportcss-loader exportType: 'string'
"css-style-sheet"생성 가능한 CSSStyleSheet를 exportcss-loader exportType: 'css-style-sheet'

module type별로 전역 설정할 수 있습니다.

export default {
  experiments: { css: true },
  module: {
    parser: {
      "css/auto": {
        exportType: "style",
      },
    },
  },
};

또는 일부 파일에만 rule별로 설정할 수 있습니다.

export default {
  experiments: { css: true },
  module: {
    rules: [
      {
        test: /\.css$/i,
        type: "css/auto",
        parser: { exportType: "style" },
      },
    ],
  },
};

Migration Guide

At a glance

Legacy setupNative equivalent
mini-css-extract-plugin (MiniCssExtractPlugin.loader)built-in extraction (default exportType: "link")
MiniCssExtractPlugin filename / chunkFilenameoutput.cssFilename / output.cssChunkFilename
style-loaderexportType: "style"
css-loaderbuilt-in CSS parsing (no loader needed)
css-loader url / importmodule.parser.css.url / import (both default true)
css-loader modules (.module.css auto-detect)css/auto module type
css-loader modules.modecss/module / css/global type + pure
css-loader modules.localIdentNamegenerator localIdentName
css-loader modules.exportLocalsConventiongenerator exportsConvention
css-loader modules.namedExportmodule.parser.css.namedExports (default true)
css-loader modules.exportOnlyLocalsgenerator exportsOnly
css-loader esModulegenerator esModule (default true)
css-loader exportType: 'string' / 'css-style-sheet'exportType: "text" / "css-style-sheet"

한 번에 loader 하나씩 마이그레이션하세요. 아래 섹션은 각 단계에서 빌드가 깨지지 않도록 하는 순서로 구성되어 있습니다.

1. Start from a classic setup

webpack.config.js

import MiniCssExtractPlugin from "mini-css-extract-plugin";

export default {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

2. Enable native CSS

webpack.config.js

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

이제 내장된 /\.css$/icss/auto rule이 .css import를 처리합니다. 아래 섹션에서 각 옵션에 대응되는 기능을 확인한 뒤, 기존의 custom rule과 plugin을 제거하세요.

3. Replace mini-css-extract-plugin

Native CSS는 기본적으로 stylesheet를 추출하고(exportType: "link"), 여기에 content hash까지 추가하므로 plugin과 그 loader가 더 이상 필요하지 않습니다.

webpack.config.js

-import MiniCssExtractPlugin from "mini-css-extract-plugin";
-
 export default {
+  experiments: {
+    css: true,
+  },
-  module: {
-    rules: [
-      {
-        test: /\.css$/i,
-        use: [MiniCssExtractPlugin.loader, "css-loader"],
-      },
-    ],
-  },
-  plugins: [new MiniCssExtractPlugin()],
 };

남은 plugin 옵션은 다음과 같이 대응됩니다.

mini-css-extract-pluginNative equivalent
filenameoutput.cssFilename
chunkFilenameoutput.cssChunkFilename
loader publicPathoutput.publicPath
loader esModulegenerator esModule (default true)
ignoreOrdern/a, Native CSS는 order-conflict warning을 발생시키지 않음

webpack.config.js

export default {
  experiments: { css: true },
  output: {
    cssFilename: "[name].[contenthash].css",
    cssChunkFilename: "[id].[contenthash].css",
  },
};

4. Replace css-loader

대부분의 css-loader 옵션은 module.parser.cssmodule.generator.css 아래에 네이티브 대응 기능이 있습니다. 일반적인 기본값(url, import, namedExports 모두 활성화)은 보통의 css-loader 설정과 이미 일치하므로, 많은 프로젝트에서는 parser 설정이 아예 필요하지 않습니다.

css-loader optionNative equivalent
urlmodule.parser.css.url, 기본값 true
importmodule.parser.css.import, 기본값 true
importLoadersn/a, 체인에 있는 loader가 @import된 파일에도 자동으로 적용됨
sourceMapdevtool로 제어됨(css별 설정 지원)
esModulemodule.generator.css.esModule, 기본값 true
exportType: 'string'parser exportType: "text"
exportType: 'css-style-sheet'parser exportType: "css-style-sheet"
modules (auto-detect)css/auto module type(내장)
modules.mode: 'local'css/module type
modules.mode: 'global'css/global type
modules.mode: 'pure'parser pure: true
modules.localIdentNamegenerator localIdentName
modules.exportLocalsConventiongenerator exportsConvention
modules.namedExportparser namedExports, 기본값 true
modules.exportOnlyLocalsgenerator exportsOnly
modules.localIdentHashSaltgenerator localIdentHashSalt
modules.localIdentHashFunctiongenerator localIdentHashFunction

예를 들어, 아래와 같은 css-loader CSS Modules 설정은

export default {
  module: {
    rules: [
      {
        test: /\.module\.css$/i,
        use: [
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[local]-[hash:base64:6]",
                exportLocalsConvention: "camel-case-only",
                namedExport: true,
              },
            },
          },
        ],
      },
    ],
  },
};

다음과 같이 바뀝니다.

webpack.config.js

export default {
  experiments: { css: true },
  module: {
    parser: {
      "css/auto": {
        namedExports: true,
      },
    },
    generator: {
      "css/auto": {
        localIdentName: "[local]-[hash:base64:6]",
        exportsConvention: "camel-case-only",
      },
    },
  },
};

일부 css-loader 옵션은 동작 방식이 다릅니다.

  • getLocalIdent: custom function 대신 Native CSS는 localIdentName 템플릿을 사용하며, 이 값도 function을 받을 수 있습니다.
  • getJSON: class-name mapping은 CSS module 자체에서 export되며 compilation의 module graph에서도 읽을 수 있으므로, framework가 디스크의 JSON 파일을 필요로 할 때는 작은 plugin으로 직렬화할 수 있습니다. server-side rendering에서는 보통 아예 필요하지 않습니다. 자세한 내용은 Server-side rendering을 참고하세요.
  • localIdentRegExp와 filter 스타일의 url/import callback은 네이티브 대응 기능이 없습니다. 해당 파일에는 계속 css-loader를 사용하거나, IgnorePlugin으로 특정 request를 제외하세요.

5. Replace style-loader

파일을 추출하는 대신 런타임에 style을 주입하기 위해 style-loader를 사용했다면 exportType: "style"을 설정하세요.

webpack.config.js

export default {
  experiments: { css: true },
  module: {
    parser: {
      "css/auto": {
        exportType: "style",
      },
    },
  },
};

이 설정은 webpack runtime에서 <style> element를 주입하므로, 기본 style-loader(injectType: "styleTag") 사용 사례를 대체할 수 있습니다. 일부 파일만 주입하고 나머지는 추출하려면 하나의 rule에만 범위를 제한하세요.

export default {
  experiments: { css: true },
  module: {
    rules: [
      {
        test: /\.inline\.css$/i,
        type: "css/auto",
        parser: { exportType: "style" },
      },
    ],
  },
};

style-loader 옵션에 대한 메모: injectType: "linkTag"는 기본 exportType: "link"(추출)에 대응합니다. attributes, insert, styleTagTransform에는 네이티브 대응 기능이 없으므로, 이 기능에 의존한다면 계속 style-loader를 사용하세요.

6. Keep using preprocessors (Sass, Less, PostCSS)

Native CSS는 CSS loader를 대체하는 것이지 preprocessor loader를 대체하는 것은 아닙니다. use에는 preprocessor loader를 유지하고, webpack이 loader의 출력을 CSS로 취급하도록 rule의 typecss/auto로 설정하세요.

webpack.config.js

export default {
  experiments: { css: true },
  module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: ["postcss-loader", "sass-loader"],
        type: "css/auto",
      },
    ],
  },
};

sass-loader는 CSS로 컴파일하고, postcss-loader는 이를 후처리하며, 그 이후에는 Native CSS가 추출, url(), CSS Modules를 담당합니다. 같은 패턴을 less-loader, stylus-loader 등에도 적용할 수 있습니다.

7. Server-side rendering (node + web)

SSR에서는 보통 두 번 빌드합니다. 브라우저용 web bundle 하나와 서버용 node bundle 하나입니다. 이때 CSS Modules의 class name이 서로 일치해야 서버에서 렌더링한 마크업이 클라이언트에서 정상적으로 hydrate됩니다. css-loadergetJSON은 이런 정보를 왕복하기 위해 자주 사용되었지만, Native CSS에서는 target 간에 localIdentName을 결정론적으로 맞춰 이 왕복 자체를 피할 수 있습니다.

모든 target에서 같은 class name이 생성되도록 path 기반 템플릿(컴파일 전체 hash 없음)을 사용하세요.

webpack.config.js

const common = {
  experiments: { css: true },
  module: {
    rules: [
      {
        test: /\.module\.css$/i,
        type: "css/module",
        generator: {
          // `[file]__[local]`은 target 간에 안정적이므로 `getJSON` 동기화가 필요 없습니다.
          localIdentName: "[file]__[local]",
        },
      },
    ],
  },
};

export default [
  { ...common, name: "web", target: "web" },
  { ...common, name: "node", target: "node" },
];

node target에서는 CSS generator의 기본값이 exportsOnly: true이므로, 서버 빌드는 class-name mapping만 export하고 stylesheet는 생성하지 않습니다. 이는 SSR renderer에 정확히 필요한 동작입니다. 브라우저 빌드는 여전히 실제 CSS를 추출합니다. 단일 config를 선호한다면 target: ["web", "node"]로 두 환경 모두에서 동작하는 universal bundle을 만들 수도 있습니다.

8. Keep imports unchanged and validate

JavaScript import는 그대로 유지됩니다.

import "./styles.css";
import * as styles from "./button.module.css";

그다음 아래 항목을 확인하세요.

  • 개발 환경에서 style이 올바르게 적용되는지
  • 프로덕션에서 추출된 .css 파일이 생성되는지
  • CSS Modules export가 기존 사용 방식과 일치하는지

All options with examples

옵션은 module.parsermodule.generator 아래에서 module type별로 설정합니다. key는 css, css/auto, css/global, css/module이며, 아래 예제는 기본 rule에 사용되는 css/auto를 기준으로 합니다.

Parser options

아래의 boolean parser 옵션은 모두 기본값이 true입니다.

OptionTypeDefaultDescription
importbooleantrue@import at-rule을 처리합니다.
urlbooleantrueurl() / image-set() / src() / image()를 처리합니다.
namedExportsbooleantrueCSS Modules local 값을 ES module named export로 내보냅니다.
exportType"link" | "style" | "text" | "css-style-sheet""link"CSS를 어떤 방식으로 출력할지 지정합니다(Output modes 참고).
purebooleanfalsestrict pure mode. 모든 selector에 local class/id가 포함되어야 합니다. css/module, css/auto에서만 사용됩니다.
as"stylesheet" | "block-contents""stylesheet"소스를 전체 stylesheet로 파싱할지, block 내부 내용으로 파싱할지 지정합니다.
animationbooleantruelocal @keyframes 이름을 변경합니다.
containerbooleantruelocal @container 이름을 변경합니다.
customIdentsbooleantruecustom identifier를 변경합니다.
dashedIdentsbooleantruedashed identifier(custom property)를 변경합니다.
functionbooleantruelocal @function 이름을 변경합니다.
gridbooleantruegrid line/area identifier를 변경합니다.
export default {
  experiments: { css: true },
  module: {
    parser: {
      "css/auto": {
        import: true,
        url: true,
        namedExports: true,
        exportType: "link",
        pure: false,
        // `@keyframes`만 이름을 바꾸고, `@container` / grid identifier는 그대로 둡니다.
        animation: true,
        container: false,
        grid: false,
      },
    },
  },
};

Generator options

OptionTypeDefaultDescription
localIdentNamestring | function"[uniqueName]-[id]-[local]" (dev) / "[fullhash]" (prod)생성할 local class name의 템플릿입니다.
exportsConvention"as-is" | "camel-case" | "camel-case-only" | "dashes" | "dashes-only" | function"as-is"export되는 local 값의 naming convention입니다.
exportsOnlybooleandocument가 없는 target(예: node)에서는 true, 그 외에는 falselocal 값만 export하고 stylesheet 생성은 건너뜁니다(SSR).
esModulebooleantrue생성되는 JS에 ES module 문법을 사용합니다.
localIdentHashFunctionstringoutput.hashFunctionlocalIdentName hash에 사용할 hash function입니다.
localIdentHashDigeststring"base64url"local ident의 hash digest 인코딩입니다.
localIdentHashDigestLengthnumber6local ident의 hash digest 길이입니다.
localIdentHashSaltstringoutput.hashSaltlocal ident에 사용할 hash salt입니다.
export default {
  experiments: { css: true },
  module: {
    generator: {
      "css/auto": {
        localIdentName: "[uniqueName]-[id]-[local]",
        exportsConvention: "camel-case-only",
        esModule: true,
        exportsOnly: false,
        localIdentHashDigest: "base64url",
        localIdentHashDigestLength: 6,
      },
    },
  },
};

exportsConventionstring 또는 string[]를 반환하는 function을 받을 수 있습니다. 배열을 반환하면 local 값을 여러 alias로 export하게 되며, 이는 css-loader와 동일한 동작입니다.

Popular examples

CSS Modules with named exports

src/app.module.css

.primary {
  color: #1f6feb;
}
.large-text {
  font-size: 2rem;
}

src/index.js

import { largeText, primary } from "./app.module.css";

document.body.classList.add(primary, largeText);

webpack.config.js

export default {
  experiments: { css: true },
  module: {
    generator: {
      "css/auto": {
        exportsConvention: "camel-case-only",
      },
    },
  },
};

Extract hashed CSS files for production

webpack.config.js

export default {
  mode: "production",
  experiments: { css: true },
  output: {
    cssFilename: "css/[name].[contenthash].css",
    cssChunkFilename: "css/[id].[contenthash].css",
  },
};

Inject <style> tags at runtime (style-loader style)

webpack.config.js

export default {
  experiments: { css: true },
  module: {
    parser: {
      "css/auto": {
        exportType: "style",
      },
    },
  },
};

Import a constructable stylesheet

src/index.js

import sheet from "./theme.css" with { type: "css" };

document.adoptedStyleSheets = [sheet];

Webpack은 with { type: "css" } import assertion을 자동으로 exportType: "css-style-sheet"에 연결하므로, CSSStyleSheet instance를 바로 얻을 수 있습니다.

Import CSS as a string

webpack.config.js

export default {
  experiments: { css: true },
  module: {
    parser: {
      "css/auto": {
        exportType: "text",
      },
    },
  },
};

src/index.js

import css from "./styles.css";

const style = new CSSStyleSheet();
style.replaceSync(css);

Global styles + scoped modules side by side

기본 css/auto rule에서는 *.module.css는 scoped로, 나머지는 전역으로 처리되므로 추가 설정이 필요 없습니다.

import "./reset.css"; // 전역
import * as card from "./card.module.css"; // 범위 지정됨

Experimental status & known limitations

experiments.css는 명시적으로 실험 단계의 기능이므로, 선택적으로 도입하고 넓게 적용하기 전에 충분히 테스트해야 합니다.

  • API와 동작은 webpack v6에서 기본값이 되기 전까지 계속 바뀔 수 있습니다.
  • 일부 loader 옵션에는 바로 대응되는 기능이 없습니다. css-loaderlocalIdentRegExp와 filter callback, style-loaderattributes / insert / styleTagTransform이 여기에 해당합니다. 이런 기능이 필요한 파일에는 해당 loader를 계속 사용하세요. (getLocalIdentlocalIdentName의 function 형태로 대응할 수 있고, getJSON/SSR은 target 간 class name 일치로 다룰 수 있습니다.)
  • importLoaders에 해당하는 기능은 없습니다. 체인에 있는 loader는 @import된 파일에도 자동으로 적용됩니다.
  • 프로젝트가 고급 loader chain에 의존하고 있다면 완전히 마이그레이션하기 전에 각 부분을 검증하세요.
Edit this page·

1 Contributor

webpack