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 옵션으로도 설정할 수 있습니다.
sideEffectssideEffects와 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.jscomponents/Button.jscomponents/Button.css이 최적화 후, 다른 최적화도 적용할 수 있습니다. 예를 들면, buttonFrom과 Button.js에서 export하는 buttonsFrom은 사용되지 않습니다. usedExports 최적화는 이를 알아채고 terser는 모듈에서 일부 명령문을 삭제할 수 있습니다.
모듈의 연결에도 적용됩니다. 따라서 이 4개의 모듈과 엔트리 모듈(그리고 아마도 좀 더 많은 의존성)을 연결할 수 있습니다. 결국 index.js에는 생성되는 코드가 없습니다.
sideEffects 플래그의 영향을 더 잘 이해하기 위해 CSS 에셋이 포함된 npm 패키지의 전체 예제와 트리 셰이킹 중에 이러한 에셋이 어떻게 영향을 받을 수 있는지 살펴보겠습니다. "awesome-ui"라는 가상의 UI 컴포넌트 라이브러리를 만들어 보겠습니다.
예제 패키지는 다음과 같습니다.
awesome-ui/
├── package.json
├── dist/
│ ├── index.js
│ ├── components/
│ │ ├── index.js
│ │ ├── Button/
│ │ │ ├── index.js
│ │ │ └── Button.css
│ │ ├── Card/
│ │ │ ├── index.js
│ │ │ └── Card.css
│ │ └── Modal/
│ │ ├── index.js
│ │ └── Modal.css
│ └── theme/
│ ├── index.js
│ └── defaultTheme.css
package.json
{
"name": "awesome-ui",
"version": "1.0.0",
"main": "dist/index.js",
"sideEffects": false
}
dist/index.js
export * from './components';
export * from './theme';
dist/components/index.js
export { default as Button } from './Button';
export { default as Card } from './Card';
export { default as Modal } from './Modal';
dist/components/Button/index.js
import './Button.css'; // 이것은 사이드 이펙트가 있습니다. 가져올 때 스타일이 적용됩니다!
export default function Button(props) {
// Button component implementation
return {
type: 'button',
...props,
};
}
dist/components/Button/Button.css
.awesome-ui-button {
background-color: #0078d7;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
dist/components/Card/index.js and dist/components/Modal/index.js would have similar structure.
dist/theme/index.js
import './defaultTheme.css'; // 이것은 사이드 이펙트가 있습니다!
export const themeColors = {
primary: '#0078d7',
secondary: '#f3f2f1',
danger: '#d13438',
};
이제 Button 구성 요소만 사용하려는 컨수머 애플리케이션을 상상해 보세요.
import { Button } from 'awesome-ui';
// 버튼 컴포넌트를 사용하세요
sideEffects: false in package.json트리 쉐이킹이 활성화된 상태에서 webpack이 이 가져오기를 처리하는 경우.
sideEffects: false가 보입니다.결과: Button 구성 요소는 렌더링되지만, 트리 셰이킹 중에 Button.css가 제거되었기 때문에 스타일이 적용되지 않습니다.
이 문제를 해결하려면 package.json을 업데이트하여 CSS 파일에 사이드 이펙트가 있음을 올바르게 표시해야 합니다.
{
"name": "awesome-ui",
"version": "1.0.0",
"main": "dist/index.js",
"sideEffects": ["**/*.css"]
}
이 설정을 사용하면,
트리 쉐이킹 중에 WebPack이 모듈을 평가하는 방식은 다음과 같습니다.
이 모듈의 내보내기 기능은 직접 사용되나요, 아니면 간접적으로 사용되나요?
모듈에 사이드 이펙트가 표시되어 있나요?
sideEffects가 이 파일을 포함하거나 true인 경우): 모듈을 포함합니다.sideEffects가 false이거나 이 파일을 포함하지 않는 경우): 모듈과 해당 종속성을 제외합니다.적절한 sideEffects 구성을 갖춘 라이브러리 파일의 경우:
dist/index.js: 직접 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 건너뛰기dist/components/index.js: 직접 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 건너뛰기dist/components/Button/index.js: 직접 내보내기 사용 -> 포함dist/components/Button/Button.css: 내보내기가 불가능하고 사이드 이펙트 있음 -> 포함dist/components/Card/*: 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 제외dist/components/Modal/*: 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 제외dist/theme/*: 내보내기가 사용되지 않음, 사이드 이펙트 없음 -> 제외잘못된 사이드 이펙트 설정은 심각한 영향을 미칠 수 있습니다.
이러한 문제는 트리 쉐이킹이 활성화된 프로덕션 빌드에서만 발생하는 경우가 많기 때문에 디버깅하기가 특히 어려울 수 있습니다.
사이드 이펙트 설정이 올바른지 테스트하는 좋은 방법은 다음과 같습니다.
/*#__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은 복잡한 의존성 트리가 있는 커다란 애플리케이션에서 작업할 때 번들의 크기를 많이 줄일 수 있습니다.
트리 쉐이킹과 sideEffects 플래그를 사용할 때 피해야 할 몇 가지 일반적인 함정이 있습니다.
sideEffects: falsepackage.json 파일에서 sideEffects: false를 설정하면 최적의 트리 셰이킹 효과를 얻을 수 있지만, 코드에 실제로 부작용이 있는 경우 문제가 발생할 수 있습니다. 숨겨진 부작용의 예는 다음과 같습니다.
다음 패턴을 고려해 보세요.
// 이 파일에는 건너뛸 수 있는 부작용이 있습니다
import './polyfill';
// 컴포넌트 다시 내보내기
export * from './components';
소비자가 특정 구성 요소만 가져오는 경우, 부작용에 대한 적절한 표시가 없으면 폴리필 가져오기가 완전히 건너뛰어질 수 있습니다.
패키지가 부작용을 올바르게 표시하더라도, 부작용을 잘못 표시하는 타사 패키지에 의존하는 경우 여전히 문제가 발생할 수 있습니다.
트리 쉐이킹은 일반적으로 프로덕션 모드에서만 완전히 활성화됩니다. 개발 모드에서만 테스트하면 배포될 때까지 트리 쉐이킹 문제를 숨길 수 있습니다.
그래서 tree shaking의 이점을 살리기 위하여
import와 export)package.json 파일에 "sideEffects" 속성을 추가하세요.production mode 설정 옵션을 사용하세요. (플래그 값을 사용하여 개발 모드에서 사이드 이펙트 최적화가 활성화됩니다)production 모드에서는 일부 기능을 사용할 수 없으므로 devtool에 올바른 값을 설정했는지 확인하세요.애플리케이션을 나무와 같이 생각할 수 있습니다. 실제로 사용되는 소스 코드와 라이브러리는 나무의 살아있는 잎과 같은 녹색을 나타냅니다. 사용하지 않는 코드는 가을에 바싹 마른 나무의 죽은 잎사귀처럼 갈색입니다. 낙엽을 없애기 위해서 나무를 흔들어서 낙엽을 떨어 뜨려야 합니다.
산출물에 대한 최적화에 더 관심이 있다면 production을 빌드하기 위한 상세 가이드로 이동하세요.