CommonJS 모듈이 애플리케이션의 트리 쉐이킹에 미치는 영향을 알아봅니다.
이 게시물에서는 CommonJS가 무엇인지, CommonJS로 인해 JavaScript 번들이 필요 이상으로 커지는 이유를 살펴봅니다.
요약: 번들러가 애플리케이션을 최적화할 수 있도록 하려면 CommonJS 모듈에 종속되지 않도록 하고 전체 애플리케이션에서 ECMAScript 모듈 문법을 사용하세요.
CommonJS란 무엇인가요?
CommonJS는 JavaScript 모듈에 대한 규칙을 확립한 2009년 표준입니다. 처음에는 웹브라우저 외부에서 주로 서버 측 애플리케이션에 사용하기 위해 고안되었습니다.
CommonJS를 사용하면 모듈을 정의하고, 모듈에서 기능을 내보내고, 다른 모듈에서 가져올 수 있습니다. 예를 들어 아래 스니펫은 add
, subtract
, multiply
, divide
, max
의 5가지 함수를 내보내는 모듈을 정의합니다.
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
나중에 다른 모듈에서 이러한 함수의 일부 또는 전부를 가져와 사용할 수 있습니다.
// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));
node
를 사용하여 index.js
를 호출하면 콘솔에 3
숫자가 출력됩니다.
2010년대 초 브라우저에 표준화된 모듈 시스템이 없기 때문에 CommonJS는 JavaScript 클라이언트 측 라이브러리에서도 인기 있는 모듈 형식이 되었습니다.
CommonJS가 최종 번들 크기에 미치는 영향은 무엇인가요?
서버 측 JavaScript 애플리케이션의 크기는 브라우저만큼 중요하지 않으므로 CommonJS는 프로덕션 번들 크기를 줄이는 것을 염두에 두고 설계되지 않았습니다. 동시에 분석에 따르면 여전히 JavaScript 번들 크기가 브라우저 앱의 속도를 저하시키는 가장 큰 이유입니다.
webpack
및 terser
와 같은 JavaScript 번들러 및 축소기는 앱 크기를 줄이기 위해 다양한 최적화를 실행합니다. 빌드 시간에 애플리케이션을 분석하여 사용하지 않는 소스 코드에서 최대한 많이 삭제하려고 합니다.
예를 들어 위 스니펫에서 add
함수만 최종 번들에 포함되어야 합니다. index.js
에서 가져오는 utils.js
의 유일한 기호이기 때문입니다.
다음 webpack
구성을 사용하여 앱을 빌드해 보겠습니다.
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'production',
};
여기서는 프로덕션 모드 최적화를 사용하고 index.js
를 진입점으로 사용하겠다고 지정합니다. webpack
를 호출한 후 출력 크기를 살펴보면 다음과 같이 표시됩니다.
$ cd dist && ls -lah
625K Apr 13 13:04 out.js
번들 크기가 625KB임을 알 수 있습니다. 출력을 살펴보면 utils.js
의 모든 함수와 lodash
의 많은 모듈을 확인할 수 있습니다. index.js
에서 lodash
를 사용하지는 않지만 출력의 일부이므로 프로덕션 애셋에 많은 가중치가 추가됩니다.
이제 모듈 형식을 ECMAScript 모듈로 변경하고 다시 시도해 보겠습니다. 이번에는 utils.js
가 다음과 같이 표시됩니다.
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
import { maxBy } from 'lodash-es';
export const max = arr => maxBy(arr);
index.js
는 ECMAScript 모듈 구문을 사용하여 utils.js
에서 가져옵니다.
import { add } from './utils.js';
console.log(add(1, 2));
동일한 webpack
구성을 사용하여 애플리케이션을 빌드하고 출력 파일을 열 수 있습니다. 이제 출력이 40바이트입니다.
(()=>{"use strict";console.log(1+2)})();
최종 번들에는 사용하지 않는 utils.js
의 함수가 포함되어 있지 않으며 lodash
의 트레이스도 없습니다. 또한 terser
(webpack
에서 사용하는 JavaScript 축소기)는 console.log
의 add
함수를 인라인 처리했습니다.
CommonJS를 사용하면 출력 번들이 거의 16,000배 커지는 이유는 무엇인가요?라는 질문이 있을 수 있습니다. 물론 이는 장난감 예시일 뿐입니다. 실제로는 크기 차이가 그렇게 크지 않을 수 있지만 CommonJS가 프로덕션 빌드에 상당한 크기를 추가할 가능성이 있습니다.
CommonJS 모듈은 ES 모듈보다 훨씬 더 동적이므로 일반적으로 최적화하기가 더 어렵습니다. 번들러와 미니파이저가 애플리케이션을 최적화할 수 있도록 하려면 CommonJS 모듈에 종속되지 않도록 하고 전체 애플리케이션에서 ECMAScript 모듈 문법을 사용하세요.
index.js
에서 ECMAScript 모듈을 사용하더라도 사용 중인 모듈이 CommonJS 모듈이면 앱의 번들 크기가 줄어듭니다.
CommonJS가 앱을 더 크게 만드는 이유는 무엇인가요?
이 질문에 답하려면 webpack
의 ModuleConcatenationPlugin
동작을 살펴본 후 정적 분석 가능성을 논의하겠습니다. 이 플러그인은 모든 모듈의 범위를 하나의 폐쇄 표현식으로 연결하고 코드가 브라우저에서 더 빠르게 실행되도록 합니다. 예를 살펴보겠습니다.
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
위에는 index.js
에서 가져오는 ECMAScript 모듈이 있습니다. subtract
함수도 정의합니다. 위와 동일한 webpack
구성을 사용하여 프로젝트를 빌드할 수 있지만, 이번에는 최소화를 사용 중지합니다.
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
minimize: false
},
mode: 'production',
};
생성된 출력을 살펴보겠습니다.
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**
/******/ })();
위 출력에서 모든 함수는 동일한 네임스페이스 내에 있습니다. 충돌을 방지하기 위해 webpack은 index.js
의 subtract
함수 이름을 index_subtract
로 변경했습니다.
최소화 도구가 위의 소스 코드를 처리하면 다음을 실행합니다.
- 사용하지 않는 함수
subtract
및index_subtract
삭제 - 모든 주석과 중복 공백 삭제
console.log
호출에서add
함수의 본문을 인라인 처리합니다.
개발자는 종종 이 사용되지 않는 가져오기 삭제를 트리 셰이킹이라고 부릅니다. 트리 쉐이킹은 webpack이 빌드 시 utils.js
에서 가져오는 기호와 내보내는 기호를 정적으로 파악할 수 있었기 때문에 가능했습니다.
이 동작은 CommonJS에 비해 더 정적으로 분석할 수 있으므로 ES 모듈에 기본적으로 사용 설정됩니다.
동일한 예시를 살펴보겠습니다. 이번에는 ES 모듈 대신 CommonJS를 사용하도록 utils.js
를 변경합니다.
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
이 작은 업데이트로 출력이 크게 달라집니다. 이 페이지에 삽입하기에는 너무 길기 때문에 일부만 공유합니다.
...
(() => {
"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));
})();
최종 번들에는 번들된 모듈에서 기능을 가져오고 내보내는 역할을 하는 삽입된 코드인 webpack
'런타임'이 포함되어 있습니다. 이번에는 utils.js
및 index.js
의 모든 기호를 동일한 네임스페이스 아래에 배치하는 대신 런타임에 __webpack_require__
를 사용하는 add
함수가 동적으로 필요합니다.
CommonJS를 사용하면 임의의 표현식에서 내보내기 이름을 가져올 수 있으므로 이 작업이 필요합니다. 예를 들어 아래 코드는 완전히 유효한 구성입니다.
module.exports[localStorage.getItem(Math.random())] = () => { … };
내보낸 기호의 이름은 런타임 시 사용자 브라우저의 컨텍스트에서만 사용할 수 있는 정보가 필요하므로 빌드 시 번들러가 알 수 있는 방법이 없습니다.
이렇게 하면 최소화 도구가 index.js
가 종속 항목에서 정확히 무엇을 사용하는지 이해할 수 없으므로 트리 셰이크를 수행할 수 없습니다. 서드 파티 모듈에서도 동일한 동작이 관찰됩니다. node_modules
에서 CommonJS 모듈을 가져오면 빌드 도구 모음에서 이를 올바르게 최적화할 수 없습니다.
CommonJS를 사용한 트리 쉐이킹
CommonJS 모듈은 정의상 동적이므로 분석하기가 훨씬 더 어렵습니다. 예를 들어 ES 모듈의 가져오기 위치는 표현식인 CommonJS에 비해 항상 문자열 리터럴입니다.
사용하는 라이브러리가 CommonJS를 사용하는 방법에 관한 특정 규칙을 따르는 경우 서드 파티 webpack
플러그인을 사용하여 빌드 시간에 사용되지 않는 내보내기를 삭제할 수 있습니다. 이 플러그인은 트리 쉐이킹 지원을 추가하지만 종속 항목에서 CommonJS를 사용할 수 있는 다양한 방법을 모두 다루지는 않습니다. 즉, ES 모듈과 동일한 보장이 제공되지 않습니다. 또한 기본 webpack
동작에 더해 빌드 프로세스의 일부로 추가 비용이 발생합니다.
결론
번들러가 애플리케이션을 최적화할 수 있도록 하려면 CommonJS 모듈에 종속되지 않도록 하고 전체 애플리케이션에서 ECMAScript 모듈 문법을 사용하세요.
다음은 최적의 경로를 따르고 있는지 확인할 수 있는 몇 가지 활용 가능한 팁입니다.
- Rollup.js의 node-resolve 플러그인을 사용하고
modulesOnly
플래그를 설정하여 ECMAScript 모듈에만 종속되도록 지정합니다. is-esm
패키지를 사용하여 npm 패키지가 ECMAScript 모듈을 사용하는지 확인합니다.- Angular를 사용하는 경우 트리 쉐이킹 작업이 불가능한 모듈에 의존하면 기본적으로 경고가 표시됩니다.