이 섹션에는 webpack이 제공하는 다양한 도구와 기능을 이해하고 마스터하기 위한 가이드가 포함되어 있습니다. 첫 번째는 시작하기를 안내하는 간단한 가이드입니다.
가이드는 계속 진행할수록 더 깊이있는 내용을 다룹니다. 대부분은 시작점 역할을 하고, 완료 후에는 실제 문서를 보다 편안하게 살펴볼 수 있습니다.
Webpack은 JavaScript 모듈을 컴파일할 때 사용됩니다. 설치하면, CLI 또는 API로 webpack과 상호 작용할 수 있습니다. 아직 webpack이 익숙하지 않은 경우 핵심 개념과 비교 내용을 통해 커뮤니티의 다른 도구보다 왜 webpack을 사용해야 할지 알아보세요.
먼저 디렉터리를 생성합니다. 그 다음 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.js
가 lodash
의 필요성을 명시적으로 선언하지 않았기 때문입니다. 단지 전역 변수인 _
가 존재하는지 추정할 뿐입니다.
이러한 방식으로 JavaScript 프로젝트를 관리하는 것은 문제가 있습니다.
대신 webpack을 사용하여 이러한 스크립트를 관리할 수 있습니다.
먼저 디렉터리 구조를 약간 수정하여 "배포" 코드(./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'
글자가 표시될 것입니다.
import 문
과 export 문
은 ES2015에서 표준화되었습니다. 현재는 대부분의 브라우저에서 지원되지만, 몇몇 브라우저에서는 새 구문을 인식하지 못합니다. 하지만 webpack은 바로 사용할 수 있도록 지원해주니 걱정하지 마세요.
보이지않는 곳에서 webpack이 실제로 코드를 "트랜스파일" 하여 이전 브라우저에서도 실행 할 수 있도록 합니다. dist/main.js
을 보면 webpack이 어떻게 트랜스파일 하는지 볼 수 있을 것입니다. 매우 독창적입니다! import
와 export
외에도 webpack은 다양한 모듈 구문을 지원합니다. 자세한 내용은 Module API에서 볼 수 있습니다.
webpack은 import
와 export
문 이외는 코드를 변경하지 않습니다. 다른 ES2015 기능을 사용한다면 webpack의 로더 시스템인 Babel을 트랜스파일러로 사용해야 합니다.
버전 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 사용보다 훨씬 많은 유연성을 제공합니다. 로더의 규칙, 플러그인, 해석 옵션 및 기타 여러 향상된 기능을 지정할 수 있습니다. 더 자세한 것은 설정 문서를 참고하세요.
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
이제 기본 빌드를 함께 완료하였습니다. 다음 가이드인 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 concepts과 configuration 페이지를 확인하세요. 또한 API에서 webpack이 제공하는 다양한 인터페이스를 자세히 살펴봅니다.
처음부터 가이드를 따라왔다면 이제 "Hello webpack"을 표시하는 작은 프로젝트가 생성되었을 것입니다. 이제 이미지와 같은 다른 애셋을 통합하고, 애셋이 어떻게 처리되는지 살펴보겠습니다.
webpack 이전에 프런트엔드 개발자는 grunt와 gulp 같은 도구를 사용하여 애셋을 처리하고 /src
폴더에서 /dist
또는 /build
디렉터리로 옮겼습니다. JavaScript 모듈에도 동일한 아이디어가 사용되었지만, webpack과 같은 도구는 모든 의존성을 동적으로 번들합니다. (디펜던시 그래프로 알려진 것을 생성합니다). 이것이 좋은 이유는 이제 모든 모듈이 의존성을 명확하게 명시하고 사용하지 않는 모듈을 번들에서 제외할 수 있기 때문입니다.
webpack의 가장 멋진 기능 중 하나는 JavaScript 외에도 로더 또는 내장 애셋 모듈이 지원하는 다른 유형의 파일도 포함 할 수 있다는 것입니다. 즉, 위에 나열된 JavaScript의 이점(예: 명시적 의존성)을 웹 사이트 또는 웹 앱을 만드는 데 사용한 모든 것에 적용할 수 있습니다. 이미 설정에 익숙 할 수 있는 CSS부터 시작해 보겠습니다.
시작하기 전에 프로젝트를 조금 변경해 보겠습니다.
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'),
},
};
JavaScript 모듈 내에서 CSS 파일을 import
하려면 style-loader와 css-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, sass 및 less 등이 있습니다.
이제 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
폴더에서 파일을 찾아서 처리했음을 의미합니다!
그렇다면 폰트와 같은 다른 애셋은 어떨까요? 애셋 모듈은 로드한 모든 파일을 가져와 빌드 디렉터리로 내보냅니다. 즉, 폰트를 포함한 모든 종류의 파일에 사용할 수 있습니다. 폰트 파일을 처리하도록 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
텍스트가 새 폰트로 변경되었는지 확인합니다. 모든 것이 잘되었다면, 변경된 폰트를 확인할 수 있을 것입니다.
로드할 수 있는 또 다른 유용한 애셋은 JSON 파일, CSV, TSV 및 XML과 같은 데이터입니다. JSON 지원은 기본으로 내장되어 있으며 NodeJS와 유사합니다. 즉, 기본적으로 import Data from './data.json'
이 동작합니다. CSV, TSV 및 XML을 가져오려면 csv-loader 및 xml-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';
특정 webpack 로더 대신 [커스텀 파서](/configuration/modules# ruleparserparse)를 사용하여 toml
, yaml
또는 json5
파일을 JSON 모듈로 가져올 수 있습니다.
src
폴더에 data.toml
, data.yaml
및 data.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
, yamljs
및 json5
패키지를 설치합니다.
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
을 확인합니다. 가져온 데이터가 콘솔에 기록되는 것을 볼 수 있습니다!
위에서 언급 한 모든 것 중 가장 멋진 점은 이러한 방식으로 애셋을 로드하면 모듈과 애셋을 보다 직관적인 방식으로 그룹화할 수 있다는 것입니다. 모든 것을 포함한 글로벌 /assets
디렉터리에 의존하는 대신 애셋을 사용하는 코드와 그룹화할 수 있습니다. 예를 들어 다음과 같은 구조가 유용할 수 있습니다.
- |- /assets
+ |– /components
+ | |– /my-component
+ | | |– index.jsx
+ | | |– index.css
+ | | |– icon.svg
+ | | |– img.png
이러한 설정은 밀접하게 연결된 모든 것이 함께 있기 때문에 코드를 다른 곳에 훨씬 더 쉽게 적용할 수 있도록 합니다. 다른 프로젝트에서 /my-component
를 사용한다고 가정해 봅시다. 간단히 복사하거나 다른 프로젝트의 /components
디렉터리로 옮기면 됩니다. 외부 의존성을 설치하고 설정에 동일한 로더가 정의되어있는 한 아무 문제가 없습니다.
그러나 예전 방식을 고수하고 있거나 여러 컴포넌트(뷰, 템플릿, 모듈 등) 간에 공유되는 애셋이 있다고 가정해 보겠습니다. 이러한 애셋을 기본 디렉터리에 저장하는 것도 가능하며 aliasing을 사용하여 쉽게 import
할 수 있습니다.
다음 가이드에서는 이 가이드에서 사용한 각기 다른 애셋을 모두 사용하지 않을 것이므로 다음 가이드인 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
이제 Output Management로 넘어가 보겠습니다.
지금까지 모든 애셋을 index.html
파일에 수동으로 포함했습니다. 하지만 애플리케이션이 커지면서 파일 이름에 해시를 사용하거나 다중 번들로 내보내기 시작하면 index.html
파일을 수동으로 관리하기 어렵습니다. 이 때 몇 가지 플러그인으로 이 프로세스를 훨씬 쉽게 관리할 수 있습니다.
먼저 프로젝트를 조금 수정해보겠습니다.
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.js
과 index.bundle.js
파일을 생성하는 것을 볼 수 있습니다. 이 파일은 index.html
파일에도 명시되어 있습니다. 브라우저에서 index.html
을 열고 버튼을 클릭하면 어떻게 되는지 확인할 수 있습니다.
그러나 엔트리 포인트 중 하나의 이름을 변경하거나 새 엔트리 포인트를 추가하면 어떻게 될까요? 생성된 번들은 빌드에서 이름이 변경되지만 index.html
파일은 여전히 예전 이름을 참조합니다. 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
저장소를 확인해 보세요.
/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
폴더를 확인해보세요. 모든 것이 잘 되었다면 이제 오래된 파일 없이 빌드에서 생성된 파일만 볼 수 있습니다!
webpack과 플러그인은 어떤 파일이 생성되는 것을 어떻게 "알고 있는지" 궁금할 것입니다. 답은 매니페스트에 있습니다. webpack은 모든 모듈이 출력 번들에 어떻게 매핑되는지 추적합니다. 만약 webpack의 output
을 다른 방식으로 관리하는데 관심이 있다면 매니페스트부터 시작하는 것이 좋습니다.
매니페스트 데이터는 WebpackManifestPlugin
을 사용하여 쉽게 적용 가능한 json 파일로 추출할 수 있습니다.
프로젝트에서 이 플러그인을 사용하는 방법에 대한 모든 예제를 다루지는 않겠지만 콘셉 페이지 및 캐싱 가이드를 읽어 보면 이것이 장기 캐싱과 어떻게 연결되는지 확인할 수 있습니다.
HTML에 번들을 동적으로 추가하는 방법을 배웠으므로 이제 개발 가이드를 살펴보세요. 또는 심화 항목을 자세히 알아보고 싶다면 코드 스플리팅 가이드를 추천합니다.
가이드를 차례대로 따라왔다면, 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,
},
};
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)에 대한 참조도 포함되어 있음을 알 수 있습니다. 이제 문제를 해결하기 위해 어디를 봐야 하는지 정확히 알 수 있습니다.
코드를 컴파일할 때마다 npm run build
를 수동으로 실행하는 것은 번거롭습니다.
webpack에는 코드가 변경될 때마다 자동으로 컴파일하는 데 도움이 되는 몇 가지 옵션이 있습니다.
대부분의 경우, webpack-dev-server
를 사용하고 싶겠지만, 위의 모든 옵션을 살펴보겠습니다.
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
를 사용해 봅시다.
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
에는 설정 가능한 많은 옵션이 있습니다. 자세한 내용은 문서를 참고하세요.
webpack-dev-middleware
는 webpack에서 처리한 파일을 서버로 내보내는 래퍼 입니다. 이것은 내부적으로 webpack-dev-server
에서 사용되지만, 사용자가 원하는 경우 더 많은 설정을 허용하기 위해 별도의 패키지로 사용할 수 있습니다. webpack-dev-middleware
와 express 서버를 결합한 예를 살펴보겠습니다.
시작하기 전에 express
와 webpack-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 앱이 실행하고 작동하는 것을 확인할 수 있습니다!
코드 자동 컴파일을 사용하면, 파일을 저장할 때 문제가 발생할 수 있습니다. 일부 편집기에는 잠재적으로 재컴파일을 방해할 수 있는 "안전한 쓰기" 기능이 있습니다.
일부 일반 편집기에서 이 기능을 비활성화하려면, 아래 목록을 참고하십시오.
atomic_save: 'false'
를 추가하십시오.Preferences > Appearance & Behavior > System Settings
에서 "Use safe write" 선택을 해제하십시오.:set backupcopy=yes
를 추가하십시오.이제 자동으로 코드를 컴파일하고 간단한 개발 서버를 실행하는 방법을 배웠으므로, 코드 스플리팅을 다룰 다음 가이드로 넘어가 볼까요?
코드 스플리팅은 webpack의 가장 매력적인 기능 중 하나입니다. 이 기능을 사용하여 코드를 다양한 번들로 분할하고, 요청에 따라 로드하거나 병렬로 로드할 수 있습니다. 더 작은 번들을 만들고 리소스 우선순위를 올바르게 제어하기 위해서 사용하며, 잘 활용하면 로드 시간에 큰 영향을 끼칠 수 있습니다.
일반적으로 코드 스플리팅은 세 가지 방식으로 접근할 수 있습니다.
entry
설정을 사용하여 코드를 수동으로 분할합니다.SplitChunksPlugin
을 사용하여 중복 청크를 제거하고 청크를 분할합니다.코드를 분할하는 가장 쉽고 직관적인 방법입니다. 그러나 다른 방법에 비해 수동적이며, 같이 살펴볼 몇 가지 함정이 있습니다. 메인 번들에서 다른 모듈을 어떻게 분리하는지 알아보겠습니다.
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
를 가져오므로 양쪽 번들에서 중복으로 포함되기 때문입니다. 다음 섹션에서 중복된 것을 제거하겠습니다.
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.js
및 another.bundle.js
외에 또 다른 runtime.bundle.js
파일이 생성됩니다.
webpack은 하나의 페이지에 여러 엔트리 포인트를 허용하지만, 가능하다면 entry: { page: ['./analytics', './app'] }
처럼 여러 개의 import가 포함된 엔트리 포인트 사용을 피해야 합니다. 이는 async
스크립트 태그를 사용할 때 최적화에 용이하며 일관된 순서로 실행할 수 있도록 합니다.
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.js
와 another.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를 분리하는데 유용합니다.
webpack은 동적 코드 스플리팅에 두 가지 유사한 기술을 지원합니다. 첫 번째이자 권장하는 접근 방식은 ECMAScript 제안을 준수하는 import()
구문을 사용하는 방식입니다. 기존의 webpack 전용 방식은 require.ensure
를 사용하는 것입니다. 이 두 가지 중 첫 번째를 사용해 보겠습니다.
시작하기 전에 위 예제의 설정에서 추가 entry
및 optimization.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);
});
Webpack 4.6.0+에서 프리페치 및 프리로드에 대한 지원이 추가되었습니다.
모듈을 가져올 때 인라인 지시문을 사용하면 webpack이 브라우저에 아래와 같은 "리소스 힌트"를 줄 수 있습니다.
간단한 프리페치의 예제를 들어보겠습니다. 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은 자체 스크립트를 생성하고 모든 에러는 시간 초과 없이 처리됩니다.
코드 스플리팅을 시작하면 출력을 분석하여 어디서 모듈이 종료되었는지 확인하는 데 유용합니다. 공식 분석 도구부터 시작하는 것이 좋습니다. 커뮤니티에서 지원하는 다른 옵션도 있습니다.
실제 애플리케이션에서 어떻게 import()
를 사용하는지 더 구체적으로 알고 싶다면 Lazy Loading의 예제를 확인하세요. 더 효율적인 코드 스플리팅 방법은 Caching을 참고하세요.
우리는 배포 가능한 /dist
디렉터리를 생성하는 모듈형 애플리케이션을 번들링하기 위해 webpack을 사용하고 있습니다. 일단 서버에 /dist
의 콘텐츠가 배포되면 클라이언트(일반적으로 브라우저)가 해당 서버에 접근하여 사이트와 애셋을 가져옵니다. 마지막 단계는 시간이 많이 걸릴 수 있기 때문에 브라우저는 캐싱이라는 기술을 사용합니다. 이렇게 하면 불필요한 네트워크 트래픽을 줄이면서 사이트를 더 빨리 로드할 수 있습니다. 그러나 새 코드를 불러올 경우에는 어려움을 느낄 수 있습니다.
이 가이드는 webpack 컴파일로 생성 된 파일의 내용이 변경되지 않는 한 캐시된 상태로 유지되도록 하는 데 필요한 설정에 초점을 맞춥니다.
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이 특정 보일러플레이트, 특히 런타임과 매니페스트를 엔트리 청크에 포함하기 때문입니다.
코드 스플릿팅에서 배운 것처럼 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에 표시된 SplitChunksPlugin
의 cacheGroups
옵션을 사용하여 수행할 수 있습니다. 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
로 줄어든 것을 볼 수 있습니다!
프로젝트에 다른 모듈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 번들 파일 이름으로 표시한 것을 알 수 있습니다.
캐싱은 복잡할 수 있지만, 애플리케이션이나 사이트 사용자에게 주는 이점으로 그만한 가치가 있습니다. 자세한 내용은 아래의 추가 자료 섹션을 참고하세요.
애플리케이션 외에도 JavaScript 라이브러리를 번들링 할 때도 webpack을 사용할 수 있습니다. 아래의 가이드는 번들링 전략을 간소화하려는 라이브러리 작성자를 위한 것입니다.
사용자가 1부터 5까지의 숫자를 숫자 표현에서 텍스트로 또는 그 반대로 변환할 수 있는 작은 라이브러리 webpack-numbers
를 작성한다고 가정해 보겠습니다. 예. 2 에서 'two'.
프로젝트의 기본 구조는 다음과 같을 것입니다.
project
+ |- webpack.config.js
+ |- package.json
+ |- /src
+ |- index.js
+ |- ref.json
npm을 초기화하고 webpack
, webpack-cli
, lodash
를 설치합니다.
npm init -y
npm install --save-dev webpack webpack-cli lodash
라이브러리에 번들 되는 것을 막고 라이브러리가 비대해지는 것을 방지하기 위해 lodash
를 dependencies
대신 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 설정으로 시작해봅시다.
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.js
를 dist/webpack-numbers.js
로 번들하도록 지시합니다.
지금까지는 애플리케이션 번들링과 동일하며 다른 점은 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 태그에서 사용할 수 있도록 번들할 것입니다.
npx webpack
을 실행하면 큰 번들이 생성 된 것을 알 수 있습니다. 파일을 검사하면 lodash가 코드와 함께 번들로 제공되는 것을 볼 수 있습니다. 이 경우 lodash
를 peer 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
라는 종속성을 사용할 수 있다고 예상한다는 것을 의미합니다.
종속성에서 여러 파일을 사용하는 라이브러리의 경우:
import A from 'library/one';
import B from 'library/two';
// ...
externals에서 library
를 지정하여 번들에서 제외할 수 없습니다. 하나씩 또는 정규식을 사용하여 제외해야 합니다.
module.exports = {
//...
externals: [
'library/one',
'library/two',
// "library/"로 시작하는 모든 것
/^library\/.+$/,
],
};
프로덕션 가이드에 언급된 단계에 따라 프로덕션에 맞게 출력을 최적화하세요. 또한 생성된 번들의 경로를 package.json
의 main
필드에 추가하세요.
package.json
{
...
"main": "dist/webpack-numbers.js",
...
}
또는 이 가이드에 따라 표준 모듈로 추가하세요.
{
...
"module": "src/index.js",
...
}
키 main
은 package.json
의 표준을, module
은 JavaScript 생태계 업그레이드가 하위 호환성을 깨지 않고 ES2015 모듈을 사용할 수 있도록 하는 제안[1] [2]을 의미합니다.
이제 사용자에게 배포하기 위해 npm 패키지로 게시하고 unpkg.com에서 찾을 수 있습니다.
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'),
},
};
};
이 가이드에는 빌드/컴파일 성능을 개선하기 위한 몇 가지 유용한 팁이 포함되어 있습니다.
다음의 모범 사례는 development 또는 production에서 빌드 스크립트를 실행하는 경우 도움이 될 것입니다.
최신 webpack 버전을 사용하세요. 우리는 항상 성능을 개선하고 있습니다. webpack의 권장 최신 버전은 다음과 같습니다.
Node.js를 최신 상태로 유지하면 성능에 도움이 될 수 있습니다. 또한 패키지 관리자(예: npm
또는 yarn
)를 최신 상태로 유지하는 것도 도움이 될 수 있습니다. 최신 버전은 더 효율적인 모듈 트리를 생성하고 해석하는 속도를 높입니다.
최소한으로 필요한 모듈에만 로더를 적용하세요.
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',
},
],
},
};
각각의 추가 로더/플러그인에는 부팅 시간이 있습니다. 가능한 한 도구를 적게 사용하세요.
아래의 단계들로 해석 속도를 향상 시킬 수 있습니다.
resolve.modules
, resolve.extensions
, resolve.mainFiles
, resolve.descriptionFiles
의 항목 수를 최소화하세요.resolve.symlinks: false
를 설정하세요(예: npm link
또는 yarn link
).resolve.cacheWithContext: false
를 설정하세요.자주 변경되지 않는 코드를 별도의 컴파일로 이동하려면 DllPlugin
을 사용하세요. 이렇게 하면 빌드 프로세스가 복잡해 지지만 애플리케이션의 컴파일 속도가 향상됩니다.
빌드 성능을 높이려면 컴파일의 총 크기를 줄이세요. 청크를 작게 유지하세요.
SplitChunksPlugin
을 사용async
모드에서 SplitChunksPlugin
을 사용thread-loader
는 작업량이 큰 로더를 worker 풀에 작업을 분담할 때 사용할 수 있습니다.
webpack 설정에서 cache
옵션을 사용하세요. package.json
의 "postinstall"
에서 캐시 디렉터리를 지우세요.
커스텀 플러그인과 로더에서 성능 문제가 발생하지 않도록 프로파일 하세요.
webpack 구성에서 ProgressPlugin
을 제거하여 빌드 시간을 단축 할 수 있습니다. ProgressPlugin
은 빠른 빌드에 유용하지 않을 수 있기 때문에 이점을 잘 활용하고 있는지 확인하세요.
다음 단계는 개발 단계에서 특히 유용합니다.
webpack의 watch 모드를 사용하세요. 다른 도구를 사용하여 파일을 보고 webpack을 호출하지 마세요. 내장된 watch 모드는 타임 스탬프를 추적하고 캐시 무효화를 위해 이 정보를 컴파일에 전달합니다.
일부 설정에서는 watch가 폴링 모드로 돌아갑니다. watch 되는 파일이 많으면 이로 인해 많은 CPU 로드가 발생할 수 있습니다. 이 경우 watchOptions.poll
을 사용하여 폴링 간격을 늘릴 수 있습니다.
아래의 유틸리티는 디스크에 쓰는 대신 메모리에서 애셋을 컴파일하고 제공하여 성능을 향상시킵니다.
webpack-dev-server
webpack-hot-middleware
webpack-dev-middleware
Webpack 4는 기본적으로 stats.toJson()
을 사용하여 많은 양의 데이터를 출력합니다. 증분 단계에서 필요한 경우가 아니면 stats
개체의 일부를 찾지 마세요. v3.1.3 이후의 webpack-dev-server
에는 증분 빌드 단계에서 stats
객체에서 검색되는 데이터의 양을 최소화하기 위한 상당한 성능 수정이 포함되었습니다.
서로 다른 devtool
설정 간의 성능 차이에 유의하세요.
"eval"
은 성능이 좋지만 트랜스파일 된 코드에는 도움이 되지 않습니다.cheap-source-map
변형은 매핑의 질이 약간 떨어지지만, 성능이 좋습니다.eval-source-map
변형을 사용합니다.특정 유틸리티, 플러그인 및 로더는 production으로 빌드할 때만 의미가 있습니다. 예를 들어, 개발 중에 TerserPlugin
을 사용하여 코드를 축소하고 조작하는 것은 일반적으로 이치에 맞지 않습니다. 이러한 도구는 일반적으로 개발 단계에서 제외되어야 합니다.
TerserPlugin
[fullhash]
/[chunkhash]
/[contenthash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
Webpack은 파일 시스템에 업데이트된 청크만 내보냅니다. 일부 설정 옵션의 경우(HMR, output.chunkFilename
,[fullhash]
안의 [name]
/[chunkhash]
/[contenthash]
) 변경된 청크와 함께 엔트리 청크가 무효화됩니다.
엔트리 청크를 작게 유지하여 내보내는 비용이 저렴한지 확인하세요. 아래의 설정은 런타임 코드에 대한 추가 청크를 생성하므로 생성 비용이 저렴합니다.
module.exports = {
// ...
optimization: {
runtimeChunk: true,
},
};
Webpack은 크기 및 부하 성능에 대한 출력을 최적화하기 위해 추가 알고리즘 작업을 수행합니다. 이러한 최적화는 작은 코드 베이스에서는 성능이 좋지만 큰 코드에서는 비용이 많이들 수 있습니다.
module.exports = {
// ...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
};
Webpack은 출력 번들에 경로 정보를 생성하는 기능이 있습니다. 그러나 이것은 수천 개의 모듈을 번들로 묶는 프로젝트에서 가비지 컬렉션에 과부화를 줍니다. options.output.pathinfo
설정에서 이 기능을 끄세요.
module.exports = {
// ...
output: {
pathinfo: false,
},
};
Node.js 버전 8.9.10 - 9.11.1의 ES2015 Map
및 Set
구현에서 성능 저하가 있었습니다. Webpack은 이러한 데이터 구조를 자유롭게 사용하므로 이 성능저하는 컴파일 시간에 영향을 줍니다.
이전 및 이후 Node.js 버전은 영향을 받지 않습니다.
ts-loader
를 사용할 때 빌드 시간을 개선하려면 transpileOnly
로더 옵션을 사용하세요. 이 옵션은 자체적으로 타입 검사를 해제합니다. 타입 검사를 다시 받으려면 ForkTsCheckerWebpackPlugin
을 사용하세요. 이렇게 각각 별도의 프로세스로 이동시키면 TypeScript 유형 검사 및 ESLint linting 속도가 빨라집니다.
module.exports = {
// ...
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
};
다음 단계는 production에서 특히 유용합니다.
소스맵은 비용이 많이 듭니다. 정말로 필요한가요?
다음 도구에는 빌드 성능을 저하시킬 수 있는 특정 문제가 있습니다.
fork-ts-checker-webpack-plugin
을 사용하세요.happyPackMode: true
/ transpileOnly: true
에서 ts-loader
를 사용합니다.node-sass
에는 Node.js 스레드 풀의 스레드를 차단하는 버그가 있습니다. thread-loader
와 함께 사용하는 경우 workerParallelJobs: 2
를 설정하세요.Webpack은 로드하는 모든 스크립트에 nonce
를 추가할 수 있습니다. 기능 세트를 활성화하려면 엔트리 스크립트에 __webpack_nonce__
변수를 포함해야 합니다. 고유한 해시 기반 nonce가 생성되고 고유한 페이지 뷰에 대해 각각 제공됩니다. 이것이 바로 __webpack_nonce__
가 설정이 아닌 엔트리 파일에 지정된 이유입니다. __webpack_nonce__
는 항상 base64로 인코딩된 문자열이어야 합니다.
엔트리 파일 안의 경우:
// ...
__webpack_nonce__ = 'c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=';
// ...
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
속성에 대한 자세한 내용은 이 페이지 하단의 더 읽어보기 섹션을 참고하세요.
Webpack은 또한 Trusted Types을 사용하여 동적으로 구성된 스크립트를 로드하고 CSP require-trusted-types-for
지시문의 제한을 준수할 수 있습니다. output.trustedTypes
설정 옵션을 참고하세요.
Vagrant를 사용하여 가상 머신에서 개발 환경을 실행하는 경우, 가상 머신에서도 webpack을 실행하고 싶을 수 있습니다.
시작하려면, 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
파일도 만들어야 합니다.
이제, 서버를 실행하세요.
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
를 변경하면 실시간으로 다시 로드됩니다.
좀 더 생산적인 환경을 모방하기 위해, 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에서 사용할 수 있도록 처리하므로 괜찮습니다.
고정 IP에서 Vagrant box에 접근할 수 있도록 만든 다음, webpack-dev-server
를 공개적으로 접근할 수 있도록 하여 브라우저에서 접근할 수 있도록 했습니다. VirtualBox가 파일 시스템 이벤트를 보내지 않아 서버가 파일 변경 시 다시 로드되지 않는 일반적인 문제를 해결했습니다.
es6 modules
commonjs
amd
요청에 표현식이 포함된 경우 컨텍스트가 생성되므로, 컴파일 시간에 정확한 모듈을 알 수 없습니다.
예를 들어, .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()
함수로 자신만의 컨텍스트를 만들 수 있습니다.
검색할 디렉터리, 하위 디렉터리를 검색해야 하는지 여부를 나타내는 플래그, 일치하는 파일의 정규식을 전달할 수 있습니다.
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`로 끝나는 파일이 있는 컨텍스트입니다.
컨텍스트 모듈은 하나의 인수(요청)를 가지는 함수를 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
에서 유용할 수 있습니다.이 가이드에서는 webpack을 설치하는 데 사용되는 다양한 방법에 대해 설명합니다.
시작하기 전에, Node.js가 최신 버전으로 설치되어 있는지 확인하세요. 현재 장기 지원 버전(LTS)은 이상적인 시작점입니다. 이전 버전에서는 webpack 혹은 관련 패키지에 필요한 기능이 누락되어 있을 수 있기 때문에 다양한 문제가 발생할 수 있습니다.
최신 webpack 릴리스는 다음과 같습니다.
최신 릴리스 또는 특정 버전을 설치하려면 다음 명령 중 하나를 실행하세요.
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"
}
다음과 같이 NPM을 설치하면 webpack을 전역적으로 사용할 수 있습니다.
npm install --global webpack
webpack이 제공하는 최신 버전을 사용하는 데 관심이 있다면 다음 명령을 사용하여 베타 버전을 설치하거나 webpack 저장소에서 직접 설치해 볼 수 있습니다.
npm install --save-dev webpack@next
# 또는 특정 tagname/branchname
npm install --save-dev webpack/webpack#<tagname/branchname>
Hot Module Replacement(또는 HMR)는 webpack에서 제공하는 가장 유용한 기능 중 하나입니다. 모든 종류의 모듈을 새로고침 할 필요 없이 런타임에 업데이트 할 수 있습니다. 이 페이지는 구현에 초점을 맞추고 개념 페이지는 작동 원리와 왜 유용한지에 대한 자세한 내용을 제공합니다.
이 기능은 생산성에 많은 도움을 줍니다. 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
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 전체 문서를 참고하세요.
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를 훨씬 쉽게 만들어주는 많은 로더가 있습니다. 그중 일부는 아래에 언급되었습니다.
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;
}
HMR이 다양한 프레임워크 및 라이브러리와 원활하게 상호 작용할 수 있도록 커뮤니티에는 다른 많은 로더와 예제가 있습니다.
--hmr
플래그를 ng serve
명령에 추가하면 됩니다.Tree shaking은 사용되지 않는 코드를 제거하기 위해 JavaScript 컨텍스트에서 일반적으로 사용되는 용어입니다. ES2015 모듈 구문은 정적 구조에 의존합니다. 예를 들면, import
와 export
가 있습니다. 이름과 개념은 ES2015 모듈 번들러의 rollup에 의해 대중화되었습니다.
webpack 2 릴리스에서는 ES2015 모듈(별칭 harmony 모듈)과 사용하지 않는 모듈의 export를 감지하는 기능을 제공합니다. 새로운 webpack 4의 릴리스는 package.json
의 "sideEffects"
프로퍼티를 통해 컴파일러에 힌트를 제공하는 방식으로 기능을 확장합니다. 프로젝트의 어떤 파일이 "순수"한지 나타내며, 만약 사용하지 않는다면 제거해도 괜찮은지 표시합니다.
다음 두 함수를 내보내는 새 유틸리티 파일인 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
를 가져오지 않지만, 여전히 번들에 포함되어 있습니다. 다음 섹션에서 수정해 보겠습니다.
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
옵션으로도 설정할 수 있습니다.
sideEffects
sideEffects
와 usedExports
(트리 쉐이킹으로 알려져 있음)의 최적화는 두 가지 다른 점이 있습니다.
sideEffects
는 전체 모듈 및 파일, 전체 하위 트리를 건너뛸 수 있기 때문에 훨씬 더 효율적입니다.
usedExports
는 terser를 사용하여 문장에서 사이드 이펙트를 감지합니다. 이것은 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
이 최적화 후, 다른 최적화도 적용할 수 있습니다. 예를 들면, buttonFrom
과 Button.js
에서 export하는 buttonsFrom
은 사용되지 않습니다. usedExports
최적화는 이를 알아채고 terser는 모듈에서 일부 명령문을 삭제할 수 있습니다.
모듈의 연결에도 적용됩니다. 따라서 이 4개의 모듈과 엔트리 모듈(그리고 아마도 좀 더 많은 의존성)을 연결할 수 있습니다. 결국 index.js
에는 생성되는 코드가 없습니다.
/*#__PURE__*/
어노테이션을 사용하여 해당 함수 호출이 사이드 이펙트가 없다(side-effect-free)(순수하다)는 것을 webpack에 알릴 수 있습니다. 함수 호출 앞에 추가하여 사이드 이펙트가 없는 것으로 표시할 수 있습니다. 함수에 전달된 인수는 어노테이션으로 표시되지 않고 개별적으로 표시해야 할 수 있습니다. 사용하지 않는 변수의 초기값이 사이드 이펙트가 없다(순수하다)면, 사용하지 않는 코드로 표시되고 실행되지 않으며 최소화할 때 삭제됩니다.
이런 동작은 optimization.innerGraph
가 true
일 때 활성화됩니다.
file.js
/*#__PURE__*/ double(55);
import
와 export
구문을 통해 "사용하지 않는 코드"를 삭제했습니다. 하지만 번들에서도 삭제해야 합니다. 이렇게 하려면 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은 복잡한 의존성 트리가 있는 커다란 애플리케이션에서 작업할 때 번들의 크기를 많이 줄일 수 있습니다.
그래서 tree shaking의 이점을 살리기 위하여
import
와 export
)package.json
파일에 "sideEffects"
속성을 추가하세요.production
mode
설정 옵션을 사용하세요. (플래그 값을 사용하여 개발 모드에서 사이드 이펙트 최적화가 활성화됩니다)production
모드에서는 일부 기능을 사용할 수 없으므로 devtool
에 올바른 값을 설정했는지 확인하세요.애플리케이션을 나무와 같이 생각할 수 있습니다. 실제로 사용되는 소스 코드와 라이브러리는 나무의 살아있는 잎과 같은 녹색을 나타냅니다. 사용하지 않는 코드는 가을에 바싹 마른 나무의 죽은 잎사귀처럼 갈색입니다. 낙엽을 없애기 위해서 나무를 흔들어서 낙엽을 떨어 뜨려야 합니다.
산출물에 대한 최적화에 더 관심이 있다면 production을 빌드하기 위한 상세 가이드로 이동하세요.
이 가이드에서 프로덕션 사이트나 애플리케이션을 구축하기 위한 유틸리티와 좋은 사례들에 대해서 자세히 알아보겠습니다.
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
에서 entry
와 output
을 설정했으며, 두 환경에서 필요한 플러그인들을 포함했습니다. webpack.dev.js
에서 mode
를 development
으로 설정했습니다. 또한, 해당 환경에 권장(강력한 소스 매핑)되는 devtool
과 간단한 devServer
설정을 추가했습니다. 마지막으로 webpack.prod.js
에 mode
를 Tree shaking 가이드에서 처음 언급했던 TerserPlugin
을 로드하기 위해 production
으로 설정 합니다.
환경별 설정에서 merge()
를 사용하여 호출하면 webpack.dev.js
및 webpack.prod.js
에 공통 설정을 포함합니다. webpack-merge
툴은 병합을 위한 다양한 고급 기능을 제공하지만, 지금 사례에서는 이런 기능이 필요하지 않습니다.
지금부터 새로운 설정 파일을 사용하기 위해 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 설정을 계속 추가하는대로 출력이 어떻게 변경되는지 위 스크립트를 자유롭게 실행하여 확인해보세요.
많은 라이브러리는 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());
Webpack v4+의 production mode
에서는 기본으로 코드를 최소화합니다.
TerserPlugin
은 최소화를 시작하고 기본으로 사용하기에 좋지만 다른 옵션도 있습니다.
만약 다른 최소화 플러그인을 사용하기로 결정했다면, 다른 플러그인이 Tree shaking 가이드에 설명 된 대로 사용하지 않는 코드를 제거하고 optimization.minimizer
를 제공하는지 확인해야 합니다.
소스맵은 디버깅뿐만 아니라 벤치마크 테스트에도 유용하므로 프로덕션에도 활성화하는 것이 좋습니다. 즉, 프로덕션용으로 추천되는 빌드 속도가 가장 빠른 것을 선택해야 합니다. (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',
});
프로덕션을 위해 CSS를 최소화하는 것이 중요합니다. Minimizing for Production을 참고하세요.
위에서 설명한 대부분의 옵션은 커맨드 라인 인자로 설정할 수 있습니다. 예를 들어 optimization.minimize은
--optimization-minimize
, 그리고 mode는
--mode
로 설정할 수 있습니다. CLI 인자의 전체 목록을 보려면 npx webpack --help=verbose
를 실행하세요.
이런 간단한 방식은 편리하지만, 좀 더 알맞은 설정을 위해 webpack 설정 파일에서 이런 옵션을 설정하는 것이 좋습니다.
지연 로딩 또는 "온 디맨드" 로딩은 사이트나 애플리케이션을 최적화하는 좋은 방법입니다. 이 방법은 기본적으로 논리적인 중단점에서 코드를 분할한 다음 유저가 새로운 코드 블록을 요구하거나 필요로 하는 작업을 수행한 후 코드를 로딩하는 것입니다. 이렇게 하면 애플리케이션의 초기 로드 속도가 빨라지고 일부 블록이 로드되지 않을 수도 있어서 전체 무게가 줄어 듭니다.
코드 스플리팅의 예제를 가져와 이 개념을 더욱 잘 보여주기 위해 약간 수정해 보겠습니다. 이 코드는 별도의 청크인 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]
...
많은 프레임워크와 라이브러리에는 방법론 안에서 구현하는 방법에 대한 자체 권고안이 있습니다. 다음은 몇 가지 예시입니다.
ECMAScript 모듈(ESM)은 웹에서 모듈을 사용하기 위한 사양입니다. 모든 최신 브라우저와 권장하는 웹 모듈 코드 작성법에서 지원됩니다.
Webpack은 ECMAScript 모듈을 최적화하기 위한 처리를 지원합니다.
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();
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를 가져오는 단축입니다.
기본적으로 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 구문을 사용할 수 없습니다.
webpack
컴파일러는 ES2015 모듈, CommonJS 또는 AMD로 작성된 모듈을 이해할 수 있습니다. 그러나 일부 써드 파티 라이브러리는 전역 종속성을 필요로 할 수 있습니다. (예: jQuery
의 경우 $
) 라이브러리는 내보낼 필요가 있는 전역 변수를 만들 수도 있습니다. 이러한 "깨진 모듈은" shimming이 작동하는 하나의 인스턴스입니다.
shimming 이 유용한 또 다른 경우는 더 많은 사용자를 지원하기 위해 브라우저 기능을 폴리필하려는 경우입니다. 이 경우 패치가 필요한 브라우저에만 해당 폴리필을 제공할 수 있습니다. (예: 요청 시 로드)
해당 글에서는 이러한 두 가지 사용 사례를 모두 살펴봅니다.
전역 변수 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
라이브러리의 나머지는 삭제되므로 트리 쉐이킹이 잘 수행됩니다.
일부 레거시 모듈은 this
가 window
객체에 의존합니다. 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());
이것은 this
가 module.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'],
}),
],
};
라이브러리가 사용자가 사용할 것으로 예상하는 전역 변수를 생성한다고 가정해 보겠습니다. 이를 증명하기 위해 작은 모듈을 추가할 수 있습니다.
프로젝트
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
를 사용하여 해당 전역 변수를 일반 모듈로 내보낼 수 있습니다. 예를 들어 file
을 file
로, helpers.parse
를 parse
로 내보내 봅시다.
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');
를 사용할 수 있으며 원활하게 작동합니다.
지금까지 논의한 대부분은 레거시 패키지 처리와 관련이 있습니다. 두 번째 주제인 폴리필로 넘어가겠습니다.
폴리필을 로드하는 방법에는 여러 가지가 있습니다. 예를 들어 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
파일이 생성되고 브라우저에서 원활하게 동작하게 됩니다. 이 설정은 개선될 수 있지만 실제로 필요한 사용자에게만 폴리필을 제공하는 방법에 대한 좋은 아이디어입니다.
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 문서를 참고하세요.
process
와 같은 Node 내장 기능은 특별한 로더나 플러그인을 사용하지 않고도 설정 파일에서 직접 폴리필 할 수 있습니다. 자세한 내용과 예제는 node 설정 페이지를 참고하세요.
레거시 모듈을 다룰 때 도움이 될 수 있는 몇 가지 도구가 있습니다.
모듈에 AMD/CommonJS 버전이 없고 dist
를 포함하려는 경우 noParse
에서 플래그를 지정할 수 있습니다. 이렇게하면 webpack이 모듈을 파싱하거나 require()
및 import
문을 해석하지 않고 모듈을 포함하게됩니다. 이 방법은 빌드 성능을 향상시키는데도 사용됩니다.
마지막으로 여러 모듈 스타일을 지원하는 모듈이 있습니다. (예: AMD, CommonJS 및 레거시의 조합) 대부분의 경우, 먼저 define
을 확인한 다음 일부 코드를 사용하여 속성을 내보냅니다. 이 경우 imports-loader
를 통해 additionalCode=var%define%20=%20false;
를 설정하여 CommonJS 경로를 강제하는 것이 도움이 될 수 있습니다.
TypeScript는 일반 JavaScript로 컴파일되고 타입이 있는 상위 집합입니다. 이 가이드에서는 TypeScript를 webpack과 통합하는 방법에 대해 알아보겠습니다.
먼저 다음을 실행하여 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.ts
의 lodash
를 import하는 부분을 변경해 보겠습니다.
./index.ts
- import _ from 'lodash';
+ import * as _ from 'lodash';
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
이 가이드에서는 ts-loader
를 사용하여 다른 웹 애셋 import 같은 추가적인 webpack 기능을 조금 더 쉽게 활성화 할 수 있습니다.
이미 babel-loader
를 사용하여 코드를 트랜스파일 하는 경우라면 @babel/preset-typescript
를 사용하여 Babel이 추가 로더를 사용하는 대신 JavaScript와 TypeScript 파일을 모두 처리하도록 합니다. ts-loader
와 달리, 기본 @babel/plugin-transform-typescript
플러그인은 어떠한 타입 검사도 수행하지 않습니다.
소스맵에 대한 자세한 내용은 개발 가이드를 참고하세요.
소스맵을 사용하려면 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'),
},
};
자세한 내용은 개발자 도구 문서를 참고하세요.
TypeScript 코드에서 import.meta.webpack
과 같은 webpack 관련 기능을 사용할 수 있습니다. 그리고 webpack은 이에 대한 타입도 제공합니다. TypeScript reference
지시문을 추가하여 선언하면 됩니다.
/// <reference types="webpack/module" />
console.log(import.meta.webpack); // 위에서 선언된 참조가 없으면 TypeScript에서 에러가 발생합니다.
npm으로부터 타사 라이브러리를 설치할 때는 해당 라이브러리에 대한 타입 정의를 설치해야 한다는 사실을 기억해야 합니다.
예를 들어, lodash를 설치하려는 경우 다음 명령을 실행해서 타입을 가져올 수 있습니다.
npm install --save-dev @types/lodash
npm 패키지가 이미 패키지 번들에 선언 유형을 포함하고 있는 경우 해당 @types
패키지를 다운로드할 필요가 없습니다. 자세한 내용은 TypeScript 변경 로그 블로그를 참고하세요.
TypeScript와 함께 비코드 애셋을 사용하려면 이러한 import에 대한 타입을 연기해야 합니다. 이를 위해서 프로젝트에 TypeScript에 대한 사용자 정의를 나타내는 custom.d.ts
파일이 필요합니다. .svg
파일에 대한 선언을 설정해 보겠습니다.
custom.d.ts
declare module '*.svg' {
const content: any;
export default content;
}
여기에서는 .svg
로 끝나는 import를 지정하고 모듈의 content
를 any
로 정의하여 SVG를 위한 새로운 모듈을 선언합니다. 타입을 문자열로 정의하여 URL이라는 것을 더 명확하게 할 수 있습니다. CSS, SCSS, JSON 등을 포함한 다른 애셋에도 동일한 개념이 적용됩니다.
빌드 도구에 대한 빌드 성능 가이드를 참고하세요.
webpack 5부터는, worker-loader
없이 Web Workers를 사용할 수 있습니다.
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
만 사용할 수 있다는 점에 유의하세요.
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,
});
};
비슷한 구문이 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 모두 지원되지 않습니다.
프로그레시브 웹 애플리케이션(또는 PWA)은 네이티브 애플리케이션과 유사한 경험을 제공하는 웹 앱입니다. PWA에 기여할 수 있는 많은 것들이 있습니다. 이 중에서 가장 중요한 것은 오프라인 일 때 앱이 작동할 수 있는 기능입니다. 이는 Service Workers라는 웹 기술을 사용하여 이루어집니다.
이 섹션에서는 앱에 오프라인 경험을 추가하는 데 중점을 둡니다. 웹 앱에 대한 오프라인 지원을 보다 쉽게 설정하는 데 도움이 될 도구를 제공하는 Workbox라는 Google 프로젝트를 사용하여 이 작업을 수행합니다.
지금까지 로컬 파일 시스템으로 직접 이동하여 출력을 확인했습니다. 일반적으로 실제 사용자는 네트워크를 통해 웹 앱에 접근합니다. 브라우저는 .html
, .js
, 그리고 .css
파일같은 필요한 애셋을 제공할 서버와 통신합니다.
간단한 서버를 사용하여 테스트해 보겠습니다. npm install http-server --save-dev
커맨드로 http-server 패키지를 설치하여 사용해 보겠습니다. 또한 package.json
의 scripts
섹션을 수정하여 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 애플리케이션을 더 이상 사용할 수 없습니다.
이것이 변경하고자 하는 것입니다. 이 문서의 끝에서는 이제 서버를 중지하고, 새로 고침을 눌러도 애플리케이션을 계속 볼 수 있습니다.
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.js
와 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
라는 2개의 추가 파일이 생성됩니다. service-worker.js
는 서비스 워커 파일이고 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
는 service-worker.js
가 실행되기 위해 필요한 파일입니다. 사용자가 생성한 파일은 다를 수 있습니다. 하지만 service-worker.js
파일은 있어야 합니다.
이제 서비스 워커를 만들었습니다. 다음 단계는 무엇일까요?
서비스 워커를 등록하여 실행 할 수 있도록 합시다. 아래의 등록 코드를 추가하면 됩니다.
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
이제 테스트해 보겠습니다. 서버를 중지하고 페이지를 새로 고침 합니다. 브라우저가 서비스 워커를 지원하는 경우 애플리케이션을 계속해서 확인할 수 있습니다. 하지만 서비스 워커가 서비스를 제공하는 것이지 서버가 제공하는 것은 아닙니다.
Workbox 프로젝트를 사용하여 오프라인 앱을 빌드했습니다. 웹 앱을 PWA로 전환하는 여정을 시작했습니다. 이제 더 나아가는 것에 대해 생각할 수 있습니다. 도움이 되는 유용한 리소스는 여기에서 찾을 수 있습니다.
publicPath
설정은 다양한 경우에서 유용하게 사용될 수 있습니다. 애플리케이션의 모든 애셋에 대한 기본 경로를 지정할 수 있습니다.
이 기능이 특히 유용한 실제 애플리케이션에서의 몇 가지 사용 사례가 있습니다. 기본적으로 output.path
디렉터리로 내보내는 모든 파일은 output.publicPath
에서 참조됩니다. 여기에는 하위 청크 (코드 스플리팅을 통해 생성됨) 및 디펜던시 그래프의 일부 애셋(예: 이미지, 글꼴 등)이 포함됩니다.
예를 들어 개발 과정에서 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),
}),
],
};
또 다른 사용 사례는 publicPath
를 직접 설정하는 것입니다. Webpack은 이를 가능하게 하는 __webpack_public_path라는__
전역 변수를 노출합니다. 따라서 애플리케이션의 엔트리 포인트에서 간단하게 처리할 수 있습니다.
__webpack_public_path__ = process.env.ASSET_PATH;
이게 전부입니다. 이미 설정에서 DefinePlugin
을 사용하고 있으므로 process.env.ASSET_PATH
는 항상 정의되어 안전하게 사용할 수 있습니다.
// entry.js
import './public-path';
import './app';
일반적인 오해를 푸는 것부터 시작하겠습니다. Webpack은 Browserify나 Brunch 같은 모듈 번들러 입니다. Make 및 Grunt, Gulp와 같은 태스크 러너가 아닙니다. 태스크 러너는 프로젝트의 린트, 빌드, 테스트와 같은 일반적인 태스크의 자동화를 처리합니다. 번들러와 비교하면 태스크 러너는 더 높은 수준에 집중합니다. 번들링의 문제는 webpack에 맡겨두고 더 높은 수준의 툴링에 대한 이점을 가질 수 있습니다.
번들러는 JavaScript와 스타일 시트의 배포를 준비하여 브라우저에 알맞은 형식으로 변환하는 것을 도와줍니다. 예를 들어 JavaScript를 최소화하거나 청크로 분리, 또는 지연 로드하여 성능을 향상 시킬 수 있습니다. 번들링은 웹 개발 환경에서 가장 중요한 과제 중 하나이며 프로세스에 많은 부하가 가지 않도록 합니다.
좋은 소식은 올바른 방법으로 접근하면 약간 중복이 있더라도 태스크 러너와 번들러를 함께 잘 사용할 수 있습니다. 이 가이드는 널리 사용되는 태스크 러너와 webpack을 어떻게 통합하는지에 대해 이해하기 쉽게 설명합니다.
webpack 사용자는 npm scripts
를 태스크 러너로 종종 사용합니다. 이것은 좋은 시작점입니다. 크로스 플랫폼(Cross-platform) 지원이 문제가 될 수 있지만, 여기엔 여러 가지 해결 방법이 있습니다. 대부분의 사용자는 아니지만 많은 사용자가 간단한 npm scripts
와 다양한 수준의 webpack 설정 및 툴링을 사용할 수 있습니다.
따라서 webpack의 핵심은 번들링에 초점을 맞추고 있지만, 태스크 러너의 일반적인 작업을 webpack으로 수행할 수 있도록 하는 다양한 확장 기능이 있습니다. 별도의 도구를 통합하면 복잡성이 늘어나기 때문에 시작하기 전에 장단점을 고려해야 합니다.
Grunt를 사용한다면 grunt-webpack
패키지를 사용하는 것이 좋습니다. grunt-webpack
을 사용하면 webpack이나 webpack-dev-server를 태스크로 실행할 수 있으며, template tags 내에서 통계에 접근 할 수 있고, 개발과 프로덕션의 설정을 분리하는 등의 작업을 수행할 수 있습니다. 설치하지 않았다면 grunt-webpack
과 webpack
의 설치를 시작하세요.
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 역시 webpack-stream
패키지를 통해 매우 간단하게 통합할 수 있습니다. (a.k.a. gulp-webpack
) 이 경우 webpack
은 webpack-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-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-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 저장소를 참고하세요.
JavaScript의 스타일에 import
를 사용하지 않는 애플리케이션(싱글 페이지 애플리케이션 혹은 다른 이유로인해)에서 CSS 및 JavaScript와 기타 파일에 대해 각각 별도의 번들을 얻기 위해 엔트리에 값 배열을 사용하여 다른 유형의 파일을 제공할 수 있습니다.
예를 들어 보겠습니다. 홈과 계정을 위한 두 가지 페이지 유형이 있는 PHP 애플리케이션이 있습니다. 홈 페이지는 다른 레이아웃을 갖고 있고, 나머지 애플리케이션(계정 페이지)과는 공유할 수 없는 JavaScript가 있습니다. 홈 페이지를 위해 애플리케이션 파일에서 home.js
와 home.css
를 출력하고, 계정 페이지를 위해 account.js
와 account.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개의 파일이 포함됩니다.
애셋 모듈은 로더를 추가로 구성하지 않아도 애셋 파일(폰트, 아이콘 등)을 사용할 수 있습니다.
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,
},
},
],
},
],
}
}
기본적으로 asset
유형은 __webpack_public_path__ + import.meta
를 수행합니다. 즉, 설정에서 output.publicPath
를 설정하면 asset
이 로드되는 URL을 재정의할 수 있습니다.
코드에서 __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';
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
파일을 출력 디렉터리로 내보내고 해당 경로를 번들에 삽입합니다. 게다가 outputPath
및 publicPath
를 사용자 지정할 수 있습니다.
파일을 출력 디렉터리로 내보낼 때 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.filename
은 output.assetModuleFilename
과 같으며 asset
및 asset/resource
모듈에서만 동작합니다.
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로 번들에 삽입됩니다.
기본적으로 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
패키지를 통해 인코딩됩니다.
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
파일은 있는 그대로 번들에 삽입됩니다.
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)
);
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은 기본 조건에 따라서 resource
와 inline
중에서 자동으로 선택합니다. 크기가 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
+ }
+ }
}
]
},
};
또한 함수를 지정하여 모듈의 인라인 여부를 결정할 수 있습니다.
Asset Modules 및 Webpack 5 이전에는, 위에 언급한 레거시 로더와 함께 inline syntax를 사용할 수 있었습니다.
현재는 모든 인라인 로더 구문을 제거하고 resourceQuery 조건을 사용하여 인라인 구문의 기능을 모방하는 것이 좋습니다.
예를 들어, raw-loader
를 asset/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: [ ... ]
+ },
+ ] }
]
},
서버 사이드 랜더링과 같은 사용 사례의 경우 애셋 방출을 비활성화할 수 있습니다. 이는 Rule.generator
하위의 emit
옵션을 통해 설정 가능합니다.
module.exports = {
// …
module: {
rules: [
{
test: /\.png$/i,
type: 'asset/resource',
generator: {
emit: false,
},
},
],
},
};
import "package"
또는 import "package/sub/path"
와 같이 모듈을 요청할 때,
패키지의 package.json
내 exports
필드에 어떤 모듈을 사용할지 선언할 수 있습니다.
이를 통해 main
필드 응답을 반환하는 기본 구현을 대체합니다.
index.js
파일은 "package"
를, 파일 시스템 조회는 "package/sub/path"
를 대체합니다.
exports
필드가 명시되면 이러한 모듈 요청만 사용 가능합니다.
그 외 다른 요청은 ModuleNotFound 오류가 발생합니다.
일반적으로 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.js | Error |
패키지 작성자는 하나의 결과 대신 여러 개의 결과를 제공할 수 있습니다. 이 경우 결과 목록을 순서대로 시도하고 첫 번째 유효한 결과를 사용합니다.
노트: 모든 유효한 결과가 아니라 첫 번째 유효한 결과만 사용합니다.
예제:
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
여기서 package/things/apple
은 .../package/good-things/apple
또는 .../package/bad-things/apple
에서 찾을 수 있습니다.
예를 들어, 다음과 같은 설정이 있습니다.
{
"exports": {
".": ["-bad-specifier-", "./non-existent.js", "./existent.js"]
}
}
Webpack 5.94.0+에서는 이전 동작이 existent.js
로 확인되었을 것이지만 non-existent.js
가 발견되지 않아 오류가 발생합니다.
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();
사용 가능한 조건은 모듈 시스템 및 도구에 따라 다릅니다.
패키지에 대한 단일 엔트리 ("."
)만 지원하는 경우 { ".": ...}
객체 중첩을 생략할 수 있습니다.
{
"exports": "./index.mjs"
}
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
각 키가 조건인 객체일 경우 프로퍼티 순서가 매우 중요합니다. 조건은 명시된 순서대로 처리됩니다.
예: { "red": "./stop.js", "green": "./drive.js"}
!= {"green": "./drive.js", "red": "./stop.js"}
(red
및 green
조건이 모두 설정된 경우 첫 번째 프로퍼티가 사용됩니다)
각 키가 하위 경로인 객체에서는 프로퍼티(하위 경로) 순서가 크게 중요하지 않습니다. 덜 구체적인 경로보다 더 구체적인 경로가 우선됩니다.
예: { "./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
필드가 우선됩니다.
기능 | 지원 |
---|---|
"." 속성 | Node.js, webpack, rollup, esinstall, wmr |
일반 속성 | Node.js, webpack, rollup, esinstall, wmr |
/ 로 끝나는 속성 | |
* 로 끝나는 속성 | Node.js, webpack, rollup, esinstall |
alternatives | Node.js, webpack, rollup, |
path에만 축약형 사용 | Node.js, webpack, rollup, esinstall, wmr |
조건에만 축약형 사용 | Node.js, webpack, rollup, esinstall, wmr |
조건 구문 | Node.js, webpack, rollup, esinstall, wmr |
중첩된 조건 구문 | Node.js, webpack, rollup, wmr(5) |
조건 순서 | Node.js, webpack, rollup, wmr(6) |
"default" 조건 | Node.js, webpack, rollup, esinstall, wmr |
경로 순서 | Node.js, webpack, rollup |
매핑되지 않았을 때 오류 | Node.js, webpack, rollup, esinstall, wmr(7) |
조건과 경로를 혼합해서 사용할 때 오류 | Node.js, webpack, rollup |
(1) Node.js 17에서 제거되었습니다. 대신 *
를 사용하세요.
(2) "./"
키는 의도적으로 무시됩니다.
(3) 프로퍼티 값은 무시되고 프로퍼티 키가 대상으로 사용됩니다. 키와 값이 동일한 경우에만 효과적으로 매핑을 허용합니다.
(4) 구문을 지원하지만, 항상 첫 번째 엔트리가 사용되므로 실제로는 사용할 수 없습니다.
(5) 다른 형제 부모 조건으로 폴백시 올바르지 않게 처리됩니다.
(6) require
조건의 경우 객체 순서가 올바르지 않게 처리됩니다. 이것은 의도적으로, wmr이 참조하는 구문과 다르지 않기 때문입니다.
(7) "exports": "./file.js"
축약형을 사용하는 경우 package/not-existing
과 같은 모든 요청은 이에 맞게 해석됩니다. 축약형을 사용하지 않는 경우 package/file.js
와 같이 직접 파일에 접근해도 오류로 이어지지 않습니다.
모듈을 참조하는 데 사용되는 구문에 따라 다음 조건 중 하나가 설정됩니다.
조건 | 설명 | 지원 |
---|---|---|
import | ESM 또는 유사한 구문에서 요청이 발생합니다. | Node.js, webpack, rollup, esinstall(1), wmr(1) |
require | CommonJs/AMD 또는 유사한 구문에서 요청이 발생합니다. | Node.js, webpack, rollup, esinstall(1), wmr(1) |
style | 스타일시트 참조에서 요청이 발생합니다. | |
sass | sass 스타일시트 참조에서 요청이 발생합니다. | |
asset | 애셋 참조에서 요청이 발생합니다. | |
script | 모듈 시스템 없이 스크립트 태그를 사용할때 요청이 발생합니다. |
부가적으로 아래의 조건도 설정할 수 있습니다.
조건 | 설명 | 지원 |
---|---|---|
module | javascript를 참조 가능한 모든 모듈 구문은 ESM을 지원합니다. ( import 또는 require 와 함께 사용했을 때) | webpack, rollup, wmr |
esmodules | 지원하는 도구에서 항상 설정합니다. | wmr |
types | type 선언과 관련 있는 typescript로부터 요청이 발생합니다. |
(1) 참조 구문에서 import
와 require
는 모두 독립적으로 설정됩니다. require
는 항상 더 낮은 우선순위를 갖습니다.
다음 구문은 import
조건을 설정합니다.
import
선언import()
표현식<script type="module">
<link rel="preload/prefetch">
new Worker(..., { type: "module" })
import
섹션import.hot.accept/decline([...])
Worklet.addModule
다음 구문은 require
조건을 설정합니다.
require(...)
define()
require([...])
require.resolve()
require.ensure([...])
require.context
module.hot.accept/decline([...])
<script src="...">
다음 구문은 style
조건을 설정합니다.
@import
<link rel="stylesheet">
다음 구문은 asset
조건을 설정합니다.
url()
new URL(..., import.meta.url)
<img src="...">
다음 구문은 script
조건을 설정합니다.
<script src="...">
script
는 모듈 시스템을 지원하지 않는 경우에만 설정해야 합니다.
CommonJs를 지원하는 시스템에서 스크립트를 전처리하는 경우,
require
로 설정해야 합니다.
이 조건은 HTML 페이지에서 스트립트 태그로 삽입할 수 있고 추가 전처리가 없는 자바스크립트 파일을 찾을 때 사용해야 합니다.
다양한 최적화를 위해 다음 조건이 설정됩니다.
조건 | 설명 | 지원 |
---|---|---|
production | 프로덕션 환경. 개발 도구를 포함하지 않아야 합니다. | webpack |
development | 개발 환경. 개발 도구를 포함해야 합니다. | webpack |
노트: production
과 development
는 모두가 사용하는 것이 아닙니다. 이 중 아무것도 설정되지 않은 경우는 가정하지 않아야 합니다.
대상 환경에 따라 다음 조건이 설정됩니다.
조건 | 설명 | 지원 |
---|---|---|
browser | Code will run in a browser. | webpack, esinstall, wmr |
electron | Code will run in electron.(1) | webpack |
worker | Code will run in a (Web)Worker.(1) | webpack |
worklet | Code will run in a Worklet.(1) | - |
node | Code will run in Node.js. | Node.js, webpack, wmr(2) |
deno | Code will run in Deno. | - |
react-native | Code will run in react-native. | - |
(1) electron
, worker
및 worklet
은 컨텍스트에 따라 node
또는 browser
와 결합합니다.
(2) 브라우저 대상 환경에 대해 설정됩니다.
각 환경에는 여러 버전이 있으므로 다음 가이드라인이 적용됩니다.
node
: 호환성은 engines
필드를 참고하세요.browser
: 패키지를 배포하는 시점의 현재 Spec 및 4단계 제안과 호환됩니다. 폴리필과 트랜스파일은 소비하는 쪽에서 처리되어야 합니다.
deno
: TBDreact-native
: TBD소스 코드를 전처리하는 도구에 따라 다음 조건이 설정됩니다.
조건 | 설명 | 지원 |
---|---|---|
webpack | webpack을 통해 처리됩니다. | webpack |
아쉽지만 Node.js 런타임에 대한 node-js
조건이 없습니다.
이것은 Node.js에 대한 예외 처리를 단순화합니다.
다음 도구는 커스텀 조건을 지원합니다.
도구 | 지원 | 노트 |
---|---|---|
Node.js | 지원 | --conditions CLI 인자를 사용. |
webpack | 지원 | resolve.conditionNames 설정 옵션을 사용. |
rollup | 지원 | @rollup/plugin-node-resolve 에서 exportConditions 옵션을 사용. |
esinstall | 미지원 | |
wmr | 미지원 |
커스텀 조건에는 다음 네이밍 스키마를 권장합니다.
<company-name>:<condition-name>
예: example-corp:beta
, google:internal
, `
패키지의 모든 패턴은 단일 "."
엔트리로 해석되지만, 각 엔트리의 패턴을 반복하여 복수의 엔트리로 확장할 수도 있습니다.
이 패턴은 엄격한 규칙이 아닌 가이드로 사용해야 합니다. 개별 패키지에 맞게 조정할 수 있습니다.
이러한 패턴은 다음과 같은 목표와 가정을 기반으로 합니다.
exports
는 향후 알려지지 않은 케이스에 대한 폴백으로 작성되어야 합니다. 이를 위해 default
조건을 사용할 수 있습니다.패키지의 의도에 따라 다른 방법이 알맞을 수 있으며 패턴은 이를 따라야 합니다. 예를 들면, 커맨드라인 도구의 경우 브라우저와 같은 미래 환경에 대한 폴백은 별로 의미가 없으며, 이 경우에는 node.js와 같은 환경 및 폴백을 대신 사용해야 합니다.
사용 케이스가 복잡할 경우 조건을 중첩하여 여러 패턴을 결합해야 합니다.
이 패턴은 환경별 API를 사용하지 않는 패키지에 적합합니다.
{
"type": "module",
"exports": "./index.js"
}
노트: ESM만 제공하면 node.js에 대한 제한이 따릅니다.
이러한 패키지는 Node.js >= 14 에서 import
를 사용할 때만 동작합니다.
require()
으로는 동작하지 않습니다.
{
"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
로 테스트 되지 않음을 의미합니다. 이중 모듈 인스턴스화로 인해 두 개의 다른 클래스가 있을 수 있기 때문입니다.
{
"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
조건을 최적화를 위해 사용합니다.
{
"type": "commonjs",
"exports": "./index.js"
}
"type": "commonjs"
를 제공하면 CommonJs 파일을 정적으로 감지할 수 있습니다.
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
dist-bundle.js
에 "type": "module"
및 .js
를 사용하더라도 이 파일은 ESM 형식이 아닙니다.
스크립트 태그로 직접 사용 할 수 있도록 전역을 사용해야 합니다.
이러한 패턴은 패키지에 개발용과 프로덕션용 두 가지 버전이 있을 때 의미가 있습니다. 예를 들면 개발 버전에는 더 나은 오류 메시지 또는 부가적인 경고를 위한 추가 코드가 포함될 수 있습니다.
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
development
조건을 지원하면 개발을 위해 향상된 버전을 사용합니다.
프로덕션 버전 또는 모드를 알 수 없는 경우에는 최적화된 버전을 사용합니다.
{
"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로 런타임을 감지해야 합니다.
모드를 감지할 수 없는 경우 프로덕션 버전으로 대체합니다.
패키지가 향후 환경을 지원할 수 있도록 폴백 환경을 선택해야 합니다. 일반적으로 브라우저와 같은 환경을 가정해야 합니다.
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
아래 예제는 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"
}
}
이 예제는 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
를 사용하여 프로덕션/개발 모드를 감지 할 수 있다고 가정하여 복잡성을 줄였습니다.
default
export를 피하십시오. 툴링 마다 다르게 처리됩니다. 명명된 export만 사용하세요..cjs
또는 type: "commonjs"
를 사용하여 소스 코드를 CommonJs로 명확하게 표시하세요. CommonJs 또는 ESM을 사용하는 경우 도구가 이를 정적으로 감지 할 수 있습니다. 이는 ESM만 지원하고 CommonJs는 지원하지 않는 도구의 경우 중요합니다.data:
URL 요청을 지원합니다.