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
을 사용하세요.