Printable

Guides

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

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

Getting Started

Webpack은 JavaScript 모듈을 컴파일할 때 사용됩니다. 설치하면, CLI 또는 API로 webpack과 상호 작용할 수 있습니다. 아직 webpack이 익숙하지 않은 경우 핵심 개념비교 내용을 통해 커뮤니티의 다른 도구보다 왜 webpack을 사용해야 할지 알아보세요.

Basic Setup

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

mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev

가이드 전반에 걸쳐 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.20"></script>
  </head>
  <body>
    <script src="./src/index.js"></script>
  </body>
</html>

또한 패키지를 private로 표기하고 main 항목을 제거하기 위해 package.json 파일을 조정해야 합니다. 이것은 실수로 코드가 출시되는 것을 방지하기 위한 것입니다.

package.json

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

예시에서 <script> 태그 사이에는 암시적인 의존성이 있습니다. index.js 파일은 실행되기 전에 페이지에 포함되는 lodash와 연관이 있습니다. 이는 index.jslodash의 필요성을 명시적으로 선언하지 않았기 때문입니다. 단지 전역 변수인 _가 존재하는지 추정할 뿐입니다.

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

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

대신 webpack을 사용하여 이러한 스크립트를 관리할 수 있습니다.

Creating a Bundle

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

project

  webpack-demo
  |- package.json
  |- package-lock.json
+ |- /dist
+   |- index.html
- |- index.html
  |- /src
    |- index.js

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

npm install --save 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.20"></script>
   </head>
   <body>
-    <script src="./src/index.js"></script>
+    <script src="main.js"></script>
   </body>
 </html>

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

그럼 npx webpack을 실행해 보겠습니다. 이 스크립트는 src/index.js엔트리 포인트로 사용하고 output으로 dist/main.js을 생성합니다. npx 명령어는 Node 8.2/npm 5.2.0 이상 버전에서 제공되며, 처음에 설치했던 webpack 패키지의 webpack 바이너리(./node_modules/.bin/webpack)를 실행합니다.

$ npx 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.4.0 compiled successfully in 1851 ms

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

Modules

import 문export 문ES2015에서 표준화되었습니다. 현재는 대부분의 브라우저에서 지원되지만, 몇몇 브라우저에서는 새 구문을 인식하지 못합니다. 하지만 webpack은 바로 사용할 수 있도록 지원해주니 걱정하지 마세요.

보이지않는 곳에서 webpack이 실제로 코드를 "트랜스파일" 하여 이전 브라우저에서도 실행 할 수 있도록 합니다. dist/main.js을 보면 webpack이 어떻게 트랜스파일 하는지 볼 수 있을 것입니다. 매우 독창적입니다! importexport 외에도 webpack은 다양한 모듈 구문을 지원합니다. 자세한 내용은 Module API에서 볼 수 있습니다.

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

Using a Configuration

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

project

  webpack-demo
  |- package.json
  |- package-lock.json
+ |- webpack.config.js
  |- /dist
    |- index.html
  |- /src
    |- index.js

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

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

$ npx 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.4.0 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.4.0",
     "webpack-cli": "^4.2.0"
   },
   "dependencies": {
     "lodash": "^4.17.20"
   }
 }

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

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

$ npm 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.4.0 compiled successfully in 1940 ms

Conclusion

이제 기본 빌드를 함께 완료하였습니다. 다음 가이드인 Asset Management로 이동하여 webpack을 이용한 이미지나 폰트 같은 애셋 관리 방법을 알아보겠습니다. 이 시점에서 프로젝트는 아래와 같아야 합니다.

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

 const path = require('path');

 module.exports = {
   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

 const path = require('path');

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

모듈 로더는 체인으로 연결할 수 있습니다. 체인의 각 로더는 리소스에 변형을 적용합니다. 체인은 역순으로 실행됩니다. 첫 번째 로더는 결과(변형이 적용된 리소스)를 다음 로더로 전달합니다. 마지막으로 webpack은 체인의 마지막 로더가 JavaScript를 반환할 것으로 예상합니다.

위의 로더 순서는 유지되어야 합니다. 'style-loader'가 먼저 오고 그 뒤에 'css-loader'가 따라오게 됩니다. 이 컨벤션을 따르지 않으면 webpack에서 오류가 발생할 수 있습니다.

이렇게 하면 스타일이 필요한 파일에 import './style.css'하여 가져올 수 있습니다. 이제 모듈이 실행될 때 html 파일의 <head>에 문자열화 된 CSS가 <style>태그로 삽입됩니다.

이제 새로운 style.css 파일을 프로젝트에 추가하고 index.js로 가져와 볼까요?

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, now imported by this script
   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.4.0 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

 const path = require('path');

 module.exports = {
   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.4.0 compiled successfully in 1972 ms

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

Loading Fonts

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

webpack.config.js

 const path = require('path');

 module.exports = {
   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.4.0 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

 const path = require('path');

 module.exports = {
   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');

   // 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을 엽니다. 개발자 도구의 콘솔에 가져온 데이터가 기록되는 것을 볼 수 있습니다!

// 경고 없음
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

 const path = require('path');
+const toml = require('toml');
+const yaml = require('yamljs');
+const json5 = require('json5');

 module.exports = {
   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

 const path = require('path');
-const toml = require('toml');
-const yaml = require('yamljs');
-const json5 = require('json5');

 module.exports = {
   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

 const path = require('path');

 module.exports = {
-  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.4.0 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

 const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');

 module.exports = {
   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.4.0 compiled successfully in 2189 ms

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

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

Cleaning up the /dist folder

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

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

webpack.config.js

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');

 module.exports = {
   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을 다른 방식으로 관리하는데 관심이 있다면 매니페스트부터 시작하는 것이 좋습니다.

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

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

Conclusion

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

Development

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

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

webpack.config.js

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');

 module.exports = {
+  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

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');

 module.exports = {
   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.4.0 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": "^4.5.0",
     "webpack": "^5.4.0",
     "webpack-cli": "^4.2.0"
   },
   "dependencies": {
     "lodash": "^4.17.20"
   }
 }

커멘드 라인에서 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

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');

 module.exports = {
   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": "^4.5.0",
     "webpack": "^5.4.0",
     "webpack-cli": "^4.2.0",
     "webpack-dev-server": "^3.11.0"
   },
   "dependencies": {
     "lodash": "^4.17.20"
   }
 }

이제 커멘드 라인에서 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

 const path = require('path');
 const HtmlWebpackPlugin = require('html-webpack-plugin');

 module.exports = {
   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

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

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

// 포트 3000에서 파일 제공
app.listen(3000, function () {
  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": "^4.17.1",
     "html-webpack-plugin": "^4.5.0",
     "webpack": "^5.4.0",
     "webpack-cli": "^4.2.0",
     "webpack-dev-middleware": "^4.0.2",
     "webpack-dev-server": "^3.11.0"
   },
   "dependencies": {
     "lodash": "^4.17.20"
   }
 }

이제 터미널에서 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.4.0 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.4.0 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

 const path = require('path');

 module.exports = {
-  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.4.0 compiled successfully in 245 ms

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

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

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

Prevent Duplication

Entry dependencies

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

webpack.config.js

 const path = require('path');

 module.exports = {
   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

 const path = require('path');

 module.exports = {
   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.4.0 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

  const path = require('path');

  module.exports = {
    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.4.0 compiled successfully in 241 ms

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

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

Dynamic Imports

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

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

webpack.config.js

 const path = require('path');

 module.exports = {
   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, now imported by this script
-  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.4.0 compiled successfully in 268 ms

import()는 promise를 반환하므로 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);
 });

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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    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

라이브러리에 번들 되는 것을 막고 라이브러리가 비대해지는 것을 방지하기 위해 lodashdependencies 대신 devDependencies로 설치합니다.

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) => {
      return ref.num === num ? ref.word : accum;
    },
    ''
  );
}

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

Webpack Configuration

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

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js',
  },
};

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

Expose the Library

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

webpack.config.js

  const path = require('path');

  module.exports = {
    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'], function (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 옵션을 업데이트해 보겠습니다.

 const path = require('path');

 module.exports = {
   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가 코드와 함께 번들로 제공되는 것을 볼 수 있습니다. 이 경우 lodashpeer dependency 로 취급하는 것이 좋습니다. 사용자는 이미 lodash가 설치되어 있어야합니다. 따라서 이 외부 라이브러리의 제어권을 라이브러리 사용자에게 넘겨야합니다.

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

webpack.config.js

  const path = require('path');

  module.exports = {
    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를 지정하여 번들에서 제외할 수 없습니다. 하나씩 또는 정규식을 사용하여 제외해야 합니다.

module.exports = {
  //...
  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 설정을 변경해야 할 사항이 있습니다. 일반적으로, module.exports는 설정 객체를 가리킵니다. env 변수를 사용하려면 module.exports를 함수로 변환해야 합니다.

webpack.config.js

const path = require('path');

module.exports = (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

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

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
      },
    ],
  },
};

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

const path = require('path');

module.exports = {
  //...
  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으로 빌드할 때만 의미가 있습니다. 예를 들어, 개발 중에 TerserPlugin을 사용하여 코드를 축소하고 조작하는 것은 일반적으로 이치에 맞지 않습니다. 이러한 도구는 일반적으로 개발 단계에서 제외되어야 합니다.

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

Minimal Entry Chunk

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

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

module.exports = {
  // ...
  optimization: {
    runtimeChunk: true,
  },
};

Avoid Extra Optimization Steps

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

module.exports = {
  // ...
  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
  },
};

Output Without Path Info

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

module.exports = {
  // ...
  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 속도가 빨라집니다.

module.exports = {
  // ...
  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 파일이 있는지 확인하세요. 만약 파일이 없다면 다음을 최소한의 예제로 사용해 시작하세요.

module.exports = {
  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

es6 modules

commonjs

amd

require with expression

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

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

example_directory
│
└───template
│   │   table.ejs
│   │   table-row.ejs
│   │
│   └───directory
│       │   another.ejs

다음의 require() 호출이 평가될 때

require('./template/' + name + '.ejs');

Webpack은 require() 호출을 구문 분석하고 일부 정보를 추출합니다.

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

컨텍스트 모듈

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

맵 예제:

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

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

이것은 동적으로 요청하는 기능이 지원되지만 모든 모듈이 번들에 포함되는 것을 의미합니다.

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(require.context('../components/', true, /\.js$/));
const cache = {};

function importAll(r) {
  r.keys().forEach((key) => (cache[key] = r(key)));
}

importAll(require.context('../components/', true, /\.js$/));
// 빌드 시 캐시는 모든 필수 모듈로 채워집니다.
  • id는 컨텍스트 모듈의 모듈 id입니다. 이것은 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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const webpack = require("webpack");

  module.exports = {
    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

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const webpack = require('webpack');
const webpackDevServer = require('webpack-dev-server');

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);

(async () => {
  await server.start();
  console.log('dev server is running');
})();

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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    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

const path = require('path');

module.exports = {
  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 { Button } from '@shopify/polaris';

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

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

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

function merge() {
  var _final = {};

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

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

  return _final;
}

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

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

var 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 할 수 있습니다.

var 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 { default as Button, buttonFrom, buttonsFrom } 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에는 생성되는 코드가 없습니다.

Mark a function call as side-effect-free

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

file.js

/*#__PURE__*/ double(55);

Minify the Output

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

webpack.config.js

const path = require('path');

module.exports = {
  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은 복잡한 의존성 트리가 있는 커다란 애플리케이션에서 작업할 때 번들의 크기를 많이 줄일 수 있습니다.

Conclusion

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

  • ES2015 모듈 구문을 사용해야 하는 것을 배웠습니다. (예: importexport)
  • 컴파일러가 ES2015 모듈 구문을 CommonJS 모듈로 변환하지 않도록 해야 합니다. (이것은 인기 있는 Babel preset @babel/preset-env의 기본 동작입니다. 자세한 내용은 documentation를 참고하세요.)
  • package.json 파일에 "sideEffects" 속성을 추가하세요.
  • 최소화와 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

+ const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+ module.exports = {
+   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

+ const { merge } = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+   mode: 'development',
+   devtool: 'inline-source-map',
+   devServer: {
+     static: './dist',
+   },
+ });

webpack.prod.js

+ const { merge } = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+   mode: 'production',
+ });

webpack.common.js에서 entryoutput을 설정했으며, 두 환경에서 필요한 플러그인들을 포함했습니다. webpack.dev.js에서 modedevelopment으로 설정했습니다. 또한, 해당 환경에 권장(강력한 소스 매핑)되는 devtool과 간단한 devServer 설정을 추가했습니다. 마지막으로 webpack.prod.jsmodeTree shaking 가이드에서 처음 언급했던 TerserPlugin을 로드하기 위해 production으로 설정 합니다.

환경별 설정에서 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": "^0.28.4",
      "csv-loader": "^2.1.1",
      "express": "^4.15.3",
      "file-loader": "^0.11.2",
      "html-webpack-plugin": "^2.29.0",
      "style-loader": "^0.18.2",
      "webpack": "^4.30.0",
      "webpack-dev-middleware": "^1.12.0",
      "webpack-dev-server": "^2.9.1",
      "webpack-merge": "^4.1.0",
      "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

  const { merge } = require('webpack-merge');
  const common = require('./webpack.common.js');

  module.exports = 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에서는 기본으로 코드를 최소화합니다.

TerserPlugin은 최소화를 시작하고 기본으로 사용하기에 좋지만 다른 옵션도 있습니다.

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

Source Mapping

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

webpack.prod.js

  const { merge } = require('webpack-merge');
  const common = require('./webpack.common.js');

  module.exports = 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

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

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]
...

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으로 가져올 수 있습니다.

import { CONSTANT, variable } from './module.js';
// 다른 모듈에 export하기 위해 "bindings"를 가져옵니다.
// 바인딩은 활성상태입니다. 값은 복사되지 않습니다.
// 대신 "변수"에 접근하면 현재 값을 얻습니다.
// 가져온 모듈에서

import * as module from './module.js';
module.fun();
// 모든 export를 가지는 "네임스페이스 객체"를 가져옵니다.

import theDefaultValue from './module.js';
// "기본" export를 가져오는 단축입니다.

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 로직 및 모듈에서 사용 가능한 심볼에 영향을 줍니다.

ESM의 import는 더 엄격하게 해석됩니다. 상대적 요청에는 fullySpecified=false로 동작을 비활성화하지 않는 한 파일 이름과 파일 확장자가 포함되어야 합니다 (예를 들면 *.js 또는 *.mjs).

ESM이 아닌 경우는 "기본" export만 가져올 수 있습니다. 명명된 export를 사용할 수 없습니다.

require, module, exports, __filename, __dirname 같은 CommonJs 구문을 사용할 수 없습니다.

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

 const path = require('path');
+const webpack = require('webpack');

 module.exports = {
   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.4.0 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

 const path = require('path');
 const webpack = require('webpack');

 module.exports = {
   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

 const path = require('path');
 const webpack = require('webpack');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  module: {
+    rules: [
+      {
+        test: require.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: function () {
    console.log('test something');
  },
  parse: function () {
    console.log('parse something');
  },
};

소스 코드에서 이러한 작업을 수행할 수는 없지만, 위에 표시된 코드와 유사한 오래된 라이브러리를 접했을 수 있습니다. 이 경우 exports-loader를 사용하여 해당 전역 변수를 일반 모듈로 내보낼 수 있습니다. 예를 들어 filefile로, helpers.parseparse로 내보내 봅시다.

webpack.config.js

 const path = require('path');
 const webpack = require('webpack');

 module.exports = {
   entry: './src/index.js',
   output: {
     filename: 'main.js',
     path: path.resolve(__dirname, 'dist'),
   },
   module: {
     rules: [
       {
         test: require.resolve('./src/index.js'),
         use: 'imports-loader?wrapper=window',
       },
+      {
+        test: require.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

 const path = require('path');
 const webpack = require('webpack');

 module.exports = {
-  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: require.resolve('./src/index.js'),
         use: 'imports-loader?wrapper=window',
       },
       {
         test: require.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를 포함하려는 경우 noParse에서 플래그를 지정할 수 있습니다. 이렇게하면 webpack이 모듈을 파싱하거나 require()import 문을 해석하지 않고 모듈을 포함하게됩니다. 이 방법은 빌드 성능을 향상시키는데도 사용됩니다.

마지막으로 여러 모듈 스타일을 지원하는 모듈이 있습니다. (예: 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
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
    |- index.js
+   |- index.ts
  |- /node_modules

tsconfig.json

JSX를 지원하도록 간단하게 설정하고 TypeScript를 ES5로 컴파일 합니다.

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node"
  }
}

tsconfig.json 설정 옵션에 대한 자세한 내용은 TypeScript 문서를 참고하세요.

webpack 설정에 대한 자세한 내용은 설정 콘셉트를 참고하세요.

이제 TypeScript를 처리하도록 webpack을 설정해 보겠습니다.

webpack.config.js

const path = require('path');

module.exports = {
  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'),
  },
};

이렇게하면 webpack이 ./index.ts 를 통해 진입하고, ts-loader를 통해 모든 .ts.tsx 파일을 로드합니다. 그리고 현재 디렉터리에 bundle.js파일을 출력합니다.

lodash의 정의에는 기본 export 표현이 없기 때문에, 이제 ./index.tslodash를 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());

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": "commonjs",
      "target": "es5",
      "jsx": "react",
      "allowJs": true,
      "moduleResolution": "node",
    }
  }

이제 webpack에 이러한 소스맵을 추출해 최종 번들에 포함되도록 지시해야 합니다.

webpack.config.js

  const path = require('path');

  module.exports = {
    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에서 에러가 발생합니다.

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만 사용할 수 있다는 점에 유의하세요.

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

self.onmessage = ({ data: { question } }) => {
  self.postMessage({
    answer: 42,
  });
};

Node.js

비슷한 구문이 12.17.0 이상의 Node.js에서 지원됩니다.

import { Worker } from '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

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');

  module.exports = {
    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';

Integrations

일반적인 오해를 푸는 것부터 시작하겠습니다. Webpack은 BrowserifyBrunch 같은 모듈 번들러 입니다. MakeGrunt, Gulp와 같은 태스크 러너가 아닙니다. 태스크 러너는 프로젝트의 린트, 빌드, 테스트와 같은 일반적인 태스크의 자동화를 처리합니다. 번들러와 비교하면 태스크 러너는 더 높은 수준에 집중합니다. 번들링의 문제는 webpack에 맡겨두고 더 높은 수준의 툴링에 대한 이점을 가질 수 있습니다.

번들러는 JavaScript와 스타일 시트의 배포를 준비하여 브라우저에 알맞은 형식으로 변환하는 것을 도와줍니다. 예를 들어 JavaScript를 최소화하거나 청크로 분리, 또는 지연 로드하여 성능을 향상 시킬 수 있습니다. 번들링은 웹 개발 환경에서 가장 중요한 과제 중 하나이며 프로세스에 많은 부하가 가지 않도록 합니다.

좋은 소식은 올바른 방법으로 접근하면 약간 중복이 있더라도 태스크 러너와 번들러를 함께 잘 사용할 수 있습니다. 이 가이드는 널리 사용되는 태스크 러너와 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: Object.assign({ watch: true }, webpackConfig),
    },
  });

  grunt.loadNpmTasks('grunt-webpack');
};

자세한 내용은 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

const gulp = require('gulp');
const webpack = require('webpack-stream');
gulp.task('default', function () {
  return 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

module.exports = 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

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  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 파일을 출력 디렉터리로 내보낼 때

이러한 로더를 대체하기 위해서 애셋 모듈에는 4개의 새로운 모듈 유형이 추가되었습니다.

  • asset/resource는 별도의 파일을 내보내고 URL을 추출합니다. 이전에는 file-loader를 사용하여 처리할 수 있었습니다.
  • asset/inline은 애셋의 data URI를 내보냅니다. 이전에는 url-loader를 사용하여 처리할 수 있었습니다.
  • asset/source는 애셋의 소스 코드를 내보냅니다. 이전에는raw-loader를 사용하여 처리할 수 있었습니다.
  • asset은 data URI와 별도의 파일 내보내기 중에서 자동으로 선택합니다. 이전에는 애셋 크기 제한이 있는 url-loader를 사용했습니다.

webpack 5의 애셋 모듈과 함께 이전 애셋 로더(예 :file-loader/url-loader/raw-loader)를 사용할 때 애셋 모듈이 애셋을 중복으로 처리하지 않도록 할 수 있습니다. 이는 애셋의 모듈 유형을 'javascript/auto'로 설정하여 적용 가능합니다.

webpack.config.js

module.exports = {
  module: {
   rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            }
          },
        ],
+       type: 'javascript/auto'
      },
   ]
  },
}

애셋 로더의 새로운 URL 호출에서 발생한 애셋을 제외하려면 로더 설정에 dependency : {not: ['url']}을 추가합니다.

webpack.config.js

module.exports = {
  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 필드를 다음과 같이 업데이트합니다.

module.exports = {
  entry: ['./publicPath.js', './App.js'],
};

또는 webpack 설정을 수정하지 않고 App.js에서 다음을 수행할 수 있습니다. 유일한 단점은 여기에서 순서를 강제해야 하고, 일부 린팅 도구와 충돌할 수 있다는 것입니다.

import './publicPath.js';

Resource assets

webpack.config.js

const path = require('path');

module.exports = {
  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

const path = require('path');

module.exports = {
  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'
      }
    ]
  },
};

특정 디렉터리에 애셋을 내보낼때 출력 파일명을 사용자 정의하는 경우도 있습니다.

const path = require('path');

module.exports = {
  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'
-     }
+     },
+     {
+       test: /\.html/,
+       type: 'asset/resource',
+       generator: {
+         filename: 'static/[hash][ext][query]'
+       }
+     }
    ]
  },
};

이 설정을 통해 모든 html 파일을 출력 디렉터리 내의 static 디렉터리로 내보내게 됩니다.

Rule.generator.filenameoutput.assetModuleFilename과 같으며 assetasset/resource 모듈에서만 동작합니다.

Inlining assets

webpack.config.js

const path = require('path');

module.exports = {
  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'
+       test: /\.svg/,
+       type: 'asset/inline'
-     },
+     }
-     {
-       test: /\.html/,
-       type: 'asset/resource',
-       generator: {
-         filename: 'static/[hash][ext][query]'
-       }
-     }
    ]
  }
};

src/index.js

- import mainImage from './images/main.png';
+ import metroMap from './images/metro.svg';

- img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
+ 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

const path = require('path');
+ const svgToMiniDataURI = require('mini-svg-data-uri');

module.exports = {
  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 assets

webpack.config.js

const path = require('path');
- const svgToMiniDataURI = require('mini-svg-data-uri');

module.exports = {
  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);
-         }
-       }
+       test: /\.txt/,
+       type: 'asset/source',
      }
    ]
  },
};

src/example.txt

Hello world

src/index.js

- import metroMap from './images/metro.svg';
+ import exampleText from './example.txt';

- block.style.background = `url(${metroMap}); // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)
+ block.textContent = exampleText; // 'Hello world'

모든 .txt 파일은 있는 그대로 번들에 삽입됩니다.

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('url').pathToFileUrl(__filename)
);

General asset type

webpack.config.js

const path = require('path');

module.exports = {
  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

const path = require('path');

module.exports = {
  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
+         }
+       }
      }
    ]
  },
};

또한 함수를 지정하여 모듈의 인라인 여부를 결정할 수 있습니다.

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: [ ... ]
+       },
+     ] }
    ]
  },

Disable emitting assets

서버 사이드 랜더링과 같은 사용 사례의 경우 애셋 방출을 비활성화할 수 있습니다. 이는 Rule.generator 하위의 emit 옵션을 통해 설정 가능합니다.

module.exports = {
  // …
  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에서 찾을 수 있습니다.

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 에서 더 이상 사용되지 않으며 *를 사용해야 합니다.

(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
if (process.env.NODE_ENV !== 'development') {
  module.exports = require('./index-optimized.cjs');
} else {
  module.exports = 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 요청을 지원합니다.
    • 기타 절대적 요청 또는 서버와 관련된 요청은 기본적으로 지원되지 않지만, 일부 도구 또는 환경에서는 지원할 수 있습니다.

1 Contributor

webpack