webpack을 사용하여 앱을 최대한 작게 만드는 방법
애플리케이션을 최적화할 때 가장 먼저 해야 할 일 중 하나는 애플리케이션을 최대한 작게 만드는 것입니다. webpack을 사용하여 이를 실행하는 방법은 다음과 같습니다.
프로덕션 모드 사용 (webpack 4만 해당)
Webpack 4에서는 새로운 mode
플래그를 도입했습니다. 이 플래그를 'development'
또는 'production'
로 설정하여 특정 환경용 애플리케이션을 빌드하고 있다고 webpack에 힌트할 수 있습니다.
// webpack.config.js
module.exports = {
mode: 'production',
};
프로덕션용 앱을 빌드할 때는 production
모드를 사용 설정해야 합니다.
이렇게 하면 webpack에서 축소, 라이브러리의 개발 전용 코드 삭제 등 기타 최적화를 적용합니다.
추가 자료
축소 사용 설정
축소는 공백을 삭제하고 변수 이름을 줄이는 등 코드를 압축하는 것입니다. 다음 행을 추가하면 됩니다.
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
↓
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}
Webpack은 코드를 축소하는 두 가지 방법, 즉 번들 수준 축소와 로더별 옵션을 지원합니다. 두 가지를 동시에 사용해야 합니다.
번들 수준 축소
번들 수준 축소는 컴파일 후 전체 번들을 압축합니다. 이용 방법은 다음과 같습니다.
다음과 같이 코드를 작성합니다.
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack은 이를 다음과 같이 컴파일합니다.
// bundle.js (part of) "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["render"] = render; /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__); function render(data, target) { console.log('Rendered!'); }
축소기는 이를 다음과 같이 대략 압축합니다.
// minified bundle.js (part of) "use strict";function t(e,n){console.log("Rendered!")} Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
webpack 4에서는 프로덕션 모드와 프로덕션 모드가 아닌 경우 모두 번들 수준 축소가 자동으로 사용 설정됩니다. 내부적으로 UglifyJS 미니파이어를 사용합니다. 축소를 사용 중지해야 하는 경우 개발 모드를 사용하거나 false
를 optimization.minimize
옵션에 전달하면 됩니다.
webpack 3에서는 UglifyJS 플러그인을 직접 사용해야 합니다. 이 플러그인은 webpack과 번들로 제공됩니다. 사용 설정하려면 구성의 plugins
섹션에 추가합니다.
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
로더별 옵션
코드를 축소하는 두 번째 방법은 로더별 옵션 (로더의 정의)입니다. 로더 옵션을 사용하면 최소화 도구로 최소화할 수 없는 항목을 압축할 수 있습니다. 예를 들어 css-loader
를 사용하여 CSS 파일을 가져오면 파일이 문자열로 컴파일됩니다.
/* comments.css */
.comment {
color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
이 코드는 문자열이므로 최소화 도구에서 압축할 수 없습니다. 파일 콘텐츠를 축소하려면 로더를 구성해야 합니다.
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
추가 자료
- UglifyJsPlugin 문서
- 기타 인기 있는 축소 도구: Babel 최소화, Google Closure 컴파일러
NODE_ENV=production
지정
프런트엔드 크기를 줄이는 또 다른 방법은 코드에서 NODE_ENV
환경 변수를 production
값으로 설정하는 것입니다.
라이브러리는 NODE_ENV
변수를 읽어 개발 모드인지 프로덕션 모드인지 감지합니다. 일부 라이브러리는 이 변수에 따라 다르게 동작합니다. 예를 들어 NODE_ENV
가 production
로 설정되지 않으면 Vue.js는 추가 검사를 실행하고 경고를 출력합니다.
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
React도 비슷하게 작동하여 경고가 포함된 개발 빌드를 로드합니다.
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
이러한 검사 및 경고는 일반적으로 프로덕션에서는 필요하지 않지만 코드에 남아 라이브러리 크기를 늘립니다. webpack 4에서는 optimization.nodeEnv: 'production'
옵션을 추가하여 삭제합니다.
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
webpack 3에서는 DefinePlugin
를 대신 사용합니다.
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
optimization.nodeEnv
옵션과 DefinePlugin
는 동일한 방식으로 작동합니다. process.env.NODE_ENV
가 나오는 모든 위치를 지정된 값으로 바꿉니다. 위의 구성을 사용하면 다음과 같이 설정할 수 있습니다.
Webpack은
process.env.NODE_ENV
의 모든 인스턴스를"production"
로 바꿉니다.// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if (process.env.NODE_ENV !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
그러면 최소화 도구가 이러한
if
브랜치를 모두 삭제합니다."production" !== 'production'
는 항상 false이고 플러그인은 이러한 브랜치 내의 코드가 실행되지 않는다는 것을 이해하기 때문입니다.// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js (without minification) if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; }
추가 자료
- '환경 변수'의 정의
DefinePlugin
,EnvironmentPlugin
에 관한 Webpack 문서
ES 모듈 사용
프런트엔드 크기를 줄이는 다음 방법은 ES 모듈을 사용하는 것입니다.
ES 모듈을 사용하면 webpack에서 트리 셰이킹을 실행할 수 있습니다. 트리 쉐이킹은 번들러가 전체 종속 항목 트리를 탐색하고 사용되는 종속 항목을 확인한 후 사용되지 않는 항목을 삭제하는 것입니다. 따라서 ES 모듈 문법을 사용하면 webpack에서 사용되지 않는 코드를 제거할 수 있습니다.
내보내기가 여러 개 있는 파일을 작성했지만 앱에서 그중 하나만 사용합니다.
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
Webpack은
commentRestEndpoint
가 사용되지 않는다고 인식하고 번들에서 별도의 내보내기 지점을 생성하지 않습니다.// bundle.js (part that corresponds to comments.js) (function(module, __webpack_exports__, __webpack_require__) { "use strict"; const render = () => { return 'Rendered!'; }; /* harmony export (immutable) */ __webpack_exports__["a"] = render; const commentRestEndpoint = '/rest/comments'; /* unused harmony export commentRestEndpoint */ })
축소 도구는 사용되지 않는 변수를 삭제합니다.
// bundle.js (part that corresponds to comments.js) (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
ES 모듈로 작성된 라이브러리에서도 작동합니다.
하지만 webpack의 내장 미니파이어 (UglifyJsPlugin
)를 정확하게 사용할 필요는 없습니다.
불필요한 코드 삭제를 지원하는 모든 최소화 도구(예: Babel Minify 플러그인 또는 Google Closure Compiler 플러그인)를 사용하면 됩니다.
추가 자료
트리 쉐이킹에 관한 Webpack 문서
이미지 최적화
이미지가 페이지 크기의 절반 이상을 차지합니다. CSS는 JavaScript만큼 중요하지는 않지만 (예: 렌더링을 차단하지 않음) 대역폭의 상당 부분을 차지합니다. url-loader
, svg-url-loader
, image-webpack-loader
를 사용하여 webpack에서 최적화합니다.
url-loader
: 작은 정적 파일을 앱에 인라인 처리합니다. 구성이 없으면 전달된 파일을 가져와 컴파일된 번들 옆에 배치하고 해당 파일의 URL을 반환합니다. 하지만 limit
옵션을 지정하면 이 제한보다 작은 파일을 Base64 데이터 URL로 인코딩하고 이 URL을 반환합니다. 이렇게 하면 이미지가 JavaScript 코드에 인라인 처리되고 HTTP 요청이 저장됩니다.
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
svg-url-loader
는 Base64가 아닌 URL 인코딩으로 파일을 인코딩한다는 점을 제외하고 url-loader
와 동일하게 작동합니다. 이는 SVG 이미지에 유용합니다. SVG 파일은 일반 텍스트이므로 이 인코딩은 크기 측면에서 더 효과적입니다.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
는 통과하는 이미지를 압축합니다. JPG, PNG, GIF, SVG 이미지를 지원하므로 이 모든 유형에 사용할 것입니다.
이 로더는 이미지를 앱에 삽입하지 않으므로 url-loader
및 svg-url-loader
와 함께 작동해야 합니다. 두 규칙 (하나는 JPG/PNG/GIF 이미지용, 다른 하나는 SVG용)에 모두 복사하여 붙여넣지 않도록 하려면 이 로더를 enforce: 'pre'
와 함께 별도의 규칙으로 포함합니다.
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre'
}
]
}
};
로더의 기본 설정은 이미 사용 가능합니다. 하지만 추가로 구성하려면 플러그인 옵션을 참고하세요. 지정할 옵션을 선택하려면 Addy Osmani의 훌륭한 이미지 최적화 가이드를 확인하세요.
추가 자료
- 'base64 인코딩은 어디에 사용되나요?'
- Addy Osmani의 이미지 최적화 가이드
종속 항목 최적화
평균 JavaScript 크기의 절반 이상이 종속 항목에서 비롯되며, 그 크기 중 일부는 불필요할 수 있습니다.
예를 들어 Lodash (v4.17.4 기준)는 번들에 최소화된 코드 72KB를 추가합니다. 하지만 메서드 중 20개만 사용한다면 약 65KB의 축소된 코드는 아무것도 하지 않습니다.
또 다른 예로는 Moment.js가 있습니다. 2.19.1 버전은 축소된 코드가 223KB나 되며 이는 매우 큽니다. 페이지의 평균 JavaScript 크기는 2017년 10월 기준 452KB였습니다. 하지만 이 중 170KB는 현지화 파일입니다. Moment.js를 여러 언어로 사용하지 않으면 이러한 파일은 목적 없이 번들을 부풀립니다.
이러한 모든 종속 항목은 쉽게 최적화할 수 있습니다. 최적화 접근 방식을 GitHub 저장소에 모았습니다. 확인해 보세요.
ES 모듈에 모듈 연결 사용 설정 (범위 호이스팅이라고도 함)
번들을 빌드할 때 webpack은 각 모듈을 함수로 래핑합니다.
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// bundle.js (part of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}
})
이전에는 CommonJS/AMD 모듈을 서로 격리하는 데 이 작업이 필요했습니다. 그러나 이렇게 하면 각 모듈의 크기와 성능 오버헤드가 추가되었습니다.
Webpack 2에서는 CommonJS 및 AMD 모듈과 달리 각 모듈을 함수로 래핑하지 않고도 번들로 묶을 수 있는 ES 모듈 지원을 도입했습니다. webpack 3에서는 모듈 연결을 통해 이러한 번들링을 가능하게 했습니다. 모듈 연결은 다음과 같은 작업을 실행합니다.
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// CONCATENATED MODULE: ./index.js
render();
})
차이를 확인할 수 있나요? 일반 번들에서 모듈 0은 모듈 1의 render
를 요구했습니다. 모듈 연결을 사용하면 require
가 필요한 함수로 간단히 대체되고 모듈 1이 삭제됩니다. 번들의 모듈 수가 줄고 모듈 오버헤드도 줄어듭니다.
이 동작을 사용 설정하려면 webpack 4에서 optimization.concatenateModules
옵션을 사용 설정하세요.
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
webpack 3에서는 ModuleConcatenationPlugin
를 사용합니다.
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
추가 자료
- ModuleConcatenationPlugin용 Webpack 문서
- '범위 호이스팅에 관한 간단한 소개'
- 이 플러그인의 역할에 관한 자세한 설명
webpack 코드와 webpack 이외의 코드가 모두 있는 경우 externals
사용
일부 코드는 webpack으로 컴파일되고 일부 코드는 컴파일되지 않는 대규모 프로젝트가 있을 수 있습니다. 동영상 호스팅 사이트와 같이 플레이어 위젯은 webpack으로 빌드될 수 있지만 주변 페이지는 빌드되지 않을 수 있습니다.

두 코드에 공통 종속 항목이 있는 경우 이를 공유하여 코드를 여러 번 다운로드하지 않아도 됩니다. 이는 webpack의 externals
옵션을 사용하여 수행됩니다. 모듈을 변수 또는 기타 외부 가져오기로 대체합니다.
window
에서 종속 항목을 사용할 수 있는 경우
webpack이 아닌 코드가 window
에서 변수로 사용할 수 있는 종속 항목을 사용하는 경우 종속 항목 이름을 변수 이름으로 별칭 지정합니다.
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
이 구성을 사용하면 webpack에서 react
및 react-dom
패키지를 번들로 묶지 않습니다. 대신 다음과 같이 대체됩니다.
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
종속 항목이 AMD 패키지로 로드되는 경우
webpack이 아닌 코드가 종속 항목을 window
에 노출하지 않으면 더 복잡해집니다.
그러나 webpack 이외의 코드가 이러한 종속 항목을 AMD 패키지로 사용하는 경우 동일한 코드를 두 번 로드하지 않을 수 있습니다.
이렇게 하려면 webpack 코드를 AMD 번들로 컴파일하고 모듈을 라이브러리 URL에 별칭으로 지정합니다.
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack은 번들을 define()
로 래핑하고 다음 URL에 종속되도록 합니다.
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
webpack 이외의 코드가 동일한 URL을 사용하여 종속 항목을 로드하는 경우 이러한 파일은 한 번만 로드됩니다. 추가 요청은 로더 캐시를 사용합니다.
추가 자료
externals
의 Webpack 문서
요약
- webpack 4를 사용하는 경우 프로덕션 모드 사용 설정
- 번들 수준의 미니파이어 및 로더 옵션으로 코드 최소화
NODE_ENV
를production
로 바꿔 개발 전용 코드를 삭제합니다.- ES 모듈을 사용하여 트리 셰이킹 사용 설정
- 이미지 압축
- 종속 항목별 최적화 적용
- 모듈 연결 사용 설정
- 적절한 경우
externals
을 사용하세요.