⚽️ 목표
CommonJS(CJS)과 ECMAScript Modules(ESM)의 차이점을 알아보자
TL;DR
ESM과 CJS는 비슷해 보이지만 전혀 다른 방식으로 작동한다.
•
ESM과 CJS는 겉보기에는 비슷해 보일 수 있으나, 전혀 다른 방식으로 동작한다는 점을 명심해야 한다.
•
ESM 기반의 모듈에서 CJS 모듈을 사용할 수 있으며 그 반대로 CJS 모듈에서 ESM 모듈을 사용할 수 있다.
◦
복잡하냐 간단하냐의 차이점이 있을 수 있지만 결론적으로 둘다 상호호환이 가능하다.
•
대부분의 최신 npm 패키지들은 ESM과 CJS 빌드 결과물을 함께 제공한다
•
앱이 커지면 커질수록 ESM 방식의 모듈들은 CJS에 비해 비교적 실행속도가 느릴 수 있다.
1. JS 모듈의 배경
모듈의 시작
•
JS 프로그램의 시작은 매우 미미했다. 초기 웹은 정적 성격이 강했기 때문에, JavaScript의 주요 역할은 독립적이고 모듈 간 소통이 필요 없는 형태로 웹에 약간의 상호작용을 추가하는 사소한 작업에 국한되었다.
◦
이로 인해 JavaScript는 초창기에 모듈 시스템을 제공하지 않았고, 모든 코드를 하나의 큰 파일에 작성하는 방식으로 코드 관리를 해야 했다. 사용 사례 자체가 복잡하지 않았기 때문에 모듈의 필요성조차 느껴지지 않았다.
•
시간이 지나면서 웹 자체에서 더 많은 상호작용이 요구되었고, 점점 더 동적인 요구사항이 생기기 시작했다. 이와 함께 JavaScript는 웹을 넘어 서버, 데스크톱 애플리케이션, 모바일 애플리케이션 등 다양한 분야에서 사용되기 시작하면서 모듈 시스템의 필요성이 부각되었다.
◦
React 기반 웹 프로젝트를 예로 들면, 모든 코드를 하나의 파일에 작성해야 하는 상황을 상상하면 모듈의 필요성을 쉽게 이해할 수 있다.
이 모든 파일에 존재하는 코드들이 한 파일에만 존재했다면?
모듈화의 시작
•
모듈 시스템이 도입되기 전, JavaScript의 주요 문제 중 하나는 네임스페이스의 부재였다. 모든 스크립트가 전역 범위에서 실행되어 어디서든 접근 가능했다.
•
CommonJS(CJS)는 2009년에 개발되어 ES6 이전의 환경에서 사용되었으며, 당시 JavaScript의 모듈 시스템 표준이 없었기에 널리 활용되었다. 이때까지 JS에는 표준으로 채택된 모듈 시스템이 존재하지 않았다.
◦
Node.js는 브라우저 외부 환경에서 JavaScript 모듈 시스템을 구현하기 위해 CommonJS를 도입했다.
•
2015년, ES6 표준 발표와 함께 ESM(ECMAScript Module)이라는 표준 모듈 시스템이 등장했다. 그러나 구체적인 구현이 부족했기 때문에 Node.js에서 실제로 지원되기까지 시간이 소요되었다.
•
CJS와 ESM 차이점을 알아보기전에 각 방식의 특징을 간단하게 알아본다.
2. CommonJS (.cjs)
•
CommonJS(CJS)는 ECMAScript Module(ESM)보다 먼저 개발되었으며, 주로 서버 사이드 환경에서 사용된다. 오랜 역사를 지닌 만큼 ESM에 비해 높은 호환성을 제공하지만, ECMAScript 표준의 일부는 아니다. 정확히 말하면, ECMAScript 표준이 도입되기 전에 널리 사용되던 모듈 시스템으로 이해하면 된다.
◦
개발 초기에는 ServerJS라고 불렸었다.
•
Node.js 환경에서 실행되는 대부분의 설정 파일은 기본적으로 CJS를 사용하며, 높은 호환성을 이유로 여전히 자주 활용된다.
•
CJS에서는 require를 사용해 모듈을 가져오며, 이 과정에서 비동기 처리가 필요하지 않다. 이는 모듈 로드가 즉시 이루어지며, Promise나 Callback을 사용할 필요가 없음을 의미한다.
◦
실행할때 모듈을 로드한다.
◦
모듈(Callee)이 로드되기 전에는 모듈을 사용하는 코드(Caller)가 실행되지 않는다.
◦
로드된 모듈은 메모리에 캐시되며, 동일한 모듈을 다시 로드할 경우 메모리에 저장된 캐시된 버전이 반환된다.
•
코드 실행 중 require() 함수를 만나면 실행이 일시적으로 중단되고 모듈이 동적으로 로드된다. 이 방식은 Tree Shaking을 어렵게 하며, 결과적으로 ESM 기반의 Tree Shaking에 비해 효율성이 떨어진다.
◦
물론 require의 함수 시그니쳐를 찾아 실제 사용여부를 알 수 있지만 근본 적으로 “실행 중 로드된다”라는 장점이자 단점 때문에 “실제 사용하는 코드를 확인”하기가 매우 어렵다.
conditional import
•
CJS에서 모듈의 로드는 동적으로 수행되므로 조건문을 사용하여 상황에 따라 다른 모듈을 import할 수 있다.
if(a === "true") {
module = require("module1");
} else {
module = require("module2");
}
JavaScript
복사
•
ES2020부터는 ESM에서도 조건부 import가 가능해졌기 때문에, 이는 더 이상 CJS만의 장점으로 볼 수 없다.
◦
ESM와 관련된 추가적인 내용은 이후에 더 상세히 다룰 예정이다.
3. ESM(.mjs)
•
자바스크립트 코드에서 표준화된 모듈화를 위해 ES6(2015년)에 도입된 모듈 관리 방식으로, 현재 유일한 ECMAScript 공식 모듈 방식이다.
•
ESM은 CJS와 달리 모듈 로드 시 정적으로 구조를 분석한 후 실행한다. 이는 모듈 종속성을 미리 분석할 수 있음을 의미한다. 이러한 특성으로 인해 사용되는 코드와 사용되지 않는 코드를 정적으로 분석할 수 있어 Tree Shaking에 유리하다.
•
모든 ESM 코드는 JavaScript의 Strict Mode가 강제적용된다.
.mjs?
•
실제 웹에서 js가 사용되어 서버로부터 js 파일을 받을 때, MIME Type이 text/javascript일 경우 브라우저는 파일 확장자는 크게 상관 없지만 v8 팀에서는 여전히 .mjs를 파일 확장자로 사용하기를 추천한다. 이유는 아래와 같다.
1.
오랜기간 동안 .cjs, 즉 CJS가 사용돼었으므로 해당 파일이 ESM 파일이라는걸 개발자들에게 명시적으로 알려주기 때문
2.
Node.js와 같은 런타임이나 Babel과 같은 툴이 모듈을 로드 시 “해당 파일은 ESM 모듈로 로드 되어야 한다.”라는 정보를 명시적으로 전달하기 위해
•
.mjs 파일을 서버로부터 serving 할 경우, Content-Type 헤더가 “text/javacript”로 설정 되어있는지 필수로 확인해야한다.
◦
Content-Type 헤더가 JavaScript MIME타입으로 설정되지 않은채로 브라우저가 서버로부터 serve 받을 경우 브라우저는 strict MIME type checking을 수행하여 에러를 출력하며 브라우저는 해당 파일을 실행 시킬 수 없다.
◦
대부분의 서버는 .js파일에 대한 Content-Type을 “text/javacript”로 serving 하지만 .mjs의 경우에는 안하는 사례가 존재하므로 다시한번 확인해봐야 한다.
브라우저에서 ESM을 사용하는 법
<script type="module">
import { add } from './math.js';
console.log(add(2, 3)); // 5
</script>
JavaScript
복사
•
브라우저 환경에서도 Node.js와 마찬가지로 CJS가 기본값이다. 따라서 ESM을 로드하려면 <script> 태그의 type 속성에 module을 지정해야 브라우저가 해당 JavaScript 파일을 ESM으로 인식하고 처리할 수 있다.
dynamic(동적) import
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", (e) => {
e.preventDefault();
import("/modules/my-module.js")
.then((module) => {
module.loadPageInto(main);
})
.catch((err) => {
main.textContent = err.message;
});
});
}
JavaScript
복사
•
ES2015에서 ESM이 처음 등장했을 때는 ESM은 동적 또는 조건부 import가 불가능했다. 이는 CommonJS와 달리 조건에 따라 다른 모듈을 import할 수 없다는 의미였다. 하지만 최신 ECMAScript 환경(ES2020 이후)에서는 조건부 및 동적 import가 가능하다.
•
자주 사용되지 않는 특정 기능을 위해 항상 모듈을 import하면 번들 크기가 커지고, 사용하지 않는 모듈 로드로 인해 성능 저하 문제가 발생할 수 있다. 이러한 문제를 해결하기 위해 ES2020에서 동적 import가 도입되었다.
•
동적 import로 로드된 모듈은 정적 분석이 불가능하므로 tree-shaking이 적용되지 않는다는 점은 잘 알려져 있다.
◦
예를 들어, my-module.js 내부에서 import된 ESM 모듈들은 tree-shaking이 가능하지만, 호출 측(callee)에서 my-module.js 자체를 tree-shaking하는 것은 불가능하다.
conditional dynamic import
async function loadModule(condition) {
let module;
if (condition === 'A') {
module = await import('./moduleA.js');
} else if (condition === 'B') {
module = await import('./moduleB.js');
} else {
module = await import('./defaultModule.js');
}
module.doSomething();
}
loadModule('A');
JavaScript
복사
•
ES2020에서 import() 함수가 도입되면서 조건부 동적 import도 가능해졌다.
4. ESM의 모듈 실행 단계
•
ESM은 Construction(Parsing) -> Instantiation -> Evaluation의 총 3단계를 통해 모듈 로딩을 수행한다
•
ESM은 CJS와 달리 코드를 실행하기 전에 모듈을 미리 로드한다는 점을 기억해야 한다. 이로 인해 ESM에서는 동일한 모듈이 여러 번 중복 import되더라도 메모리에는 단 한 번만 로드된다.
◦
CJS는 ESM과 다르게 코드가 실행될 때 로드하므로 여러번 import 할 경우 실제로도 여러번 import 한다. 하지만 메모리에 캐싱 되어있는 모듈을 다시 반환할 뿐이다.
•
HTML 명세는 모듈을 어떻게 로드할지에 대한 책임을 지며, ES Module 명세는 모듈을 어떻게 파싱, 인스턴스화, 평가할지에 대한 책임을 진다.
◦
HTML 명세는 브라우저가 <script> 태그를 사용하여 JavaScript 파일을 로드하고 실행하는 과정을 정의한다. 이 과정에는 모듈의 다운로드, 네트워크 요청 관리, 파일 위치 해석, 캐싱 정책 등이 포함된다. 즉, HTML 명세는 모듈 파일이 물리적으로 브라우저로 전달되는 단계에 초점을 맞춘다.
◦
ES Module 명세는 브라우저가 로드된 모듈을 어떻게 파싱(Parsing), 인스턴스화(Instantiation), 평가(Evaluation)할지를 정의한다. 이는 모듈의 종속성을 해결하고, 모듈에서 정의된 변수, 함수, 클래스 등을 실행 환경에 연결하며, 실제로 코드를 실행하여 결과를 생성하는 단계에 초점을 맞춘다.
Module Records
•
Module Records는 파일 내 모듈 간 관계를 나타내는 데이터 구조이다. ECMAScript 명세를 참조하여 이를 확인하면 전체적인 이해가 쉽다.
•
개념적 모델을 구상하기 위해, 아래와 같은 내용을 가진 example.mjs 파일이 있다고 가정한다.
import x from "./moduleX";
import * as ns from "./moduleY";
import { y as z } from "./moduleZ";
export var a = 10;
export default function b() {}
export { c as d } from "./otherModule";
export * from "./extraModule";
JavaScript
복사
•
JS 엔진마다 구현 방식은 다르며 실제로 더 많은 메서드가 포함될 수 있다. 그러나 example.mjs의 Module Record는 다음과 유사한 형태로 생성된다.
{
"ECMAScriptCode": "Parsed result of exampleModule.js",
"RequestedModules": [
"./moduleX",
"./moduleY",
"./moduleZ",
"./otherModule",
"./extraModule"
],
"ImportEntries": [
{
"ModuleRequest": "./moduleX",
"ImportName": "default",
"LocalName": "x"
},
{
"ModuleRequest": "./moduleY",
"ImportName": "*",
"LocalName": "ns"
},
{
"ModuleRequest": "./moduleZ",
"ImportName": "y",
"LocalName": "z"
}
],
"LocalExportEntries": [
{
"ExportName": "a",
"ModuleRequest": null,
"ImportName": null,
"LocalName": "a"
},
{
"ExportName": "default",
"ModuleRequest": null,
"ImportName": null,
"LocalName": "b"
}
],
"IndirectExportEntries": [
{
"ExportName": "d",
"ModuleRequest": "./otherModule",
"ImportName": "c",
"LocalName": null
}
],
"StarExportEntries": [
{
"ExportName": null,
"ModuleRequest": "./extraModule",
"ImportName": "*",
"LocalName": null
}
]
}
JSON
복사
a. Construction (Parsing)
파일에서 모듈로 import 하는 파일 파악 및 다운로드 후 파일을 파싱하여 Module records 생성
•
import 구문만을 탐색하며 재귀적으로 각 파일에서 모든 모듈을 로드한다.
1.
main.mjs에서 처음 임포트된 moduleA.js로 이동한다.
2.
moduleA.mjs에서 moduleB.js로 이동하는 import 문을 발견하여 moduleB.mjs로 이동한다.
3.
moduleB.mjs에는 추가적인 import가 없으므로 moduleA.mjs로 돌아간다.
4.
moduleA.js에서도 moduleB.js를 제외한 추가 import가 없으므로 main.mjs로 돌아간다.
5.
main.mjs에 b.js를 임포트하는 문이 있지만, 해당 모듈이 이미 탐색되었으므로 이를 무시한다.
•
이러한 방식으로 수집된 정보를 기반으로 각 파일의 Module Record를 생성한다.
•
이 과정에서는 import된 코드를 직접 실행하지 않으나 이 과정에서 import 구문 내 오타 및 존재하지 않는 모듈에 대한 참조와 같은 에러를 탐지할 수 있다.
b. Instantiation
•
JS 엔진은 Module Environment Record를 생성하여 변수를 관리하고, export를 위한 메모리 공간을 할당한다. 하지만 이 메모리 공간은 평가 단계에서 값이 채워진다.
•
Instantiation 단계에서는 Construction 단계에서 생성된 각 파일의 Module Record를 Module Environment Record를 사용해 서로 연결하고, 이를 통해 동일한 메모리 주소를 참조하게 한다.
c. Evaluation
•
코드를 실행하여 Instantiation 단계에서 생성된 모든 요소가 실제 값을 갖도록 한다.
•
이 과정에서 코드는 의존성의 최하위부터 실행되며, 마지막으로 main.mjs가 실행된다.
◦
main.mjs는 다른 모듈들(moduleA, moduleB)을 import한 메인 코드로, 최종적으로 실행되는 단계이다.
5. 서로 다른 모듈시스템 사용하기
ESM에서 CJS import
import cjsModule from './foo.cjs';
const {foo} = cjsModule;
JavaScript
복사
•
createRequire 함수를 사용하면 named import 방식으로 모듈을 사용할 수 있다. 그러나 이 방식은 코드가 다소 장황해지므로, 단순히 import를 사용한 후 객체 구조 분해를 수행하는 방법이 더 간단하고 권장된다.
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const {foo} = require('./foo.cjs');
JavaScript
복사
CJS에서 ESM import
•
최신 Node.js 버전에서는 import() 함수를 사용하여 ESM을 다룰 수 있지만, Top-level async 특성으로 인해 코드가 상당히 장황해질 수 있다.
(async () => {
const {foo} = await import('./foo.mjs');
})();
JavaScript
복사
•
Babel을 이용하면 require()를 통해 ESM 모듈을 가져올 수 있다. 하지만, 이 방법은 추가 설정이 필요하므로 최신 Node.js 환경에서는 권장되지 않는다.
◦
현재 Node.js에서는 require() 함수로 Babel이나 기타 트랜스파일러 없이 ESM을 사용하는 방법이 실험적 기능으로 제공된다. 이 방법은 await import보다 더 빠른 속도를 제공하므로, 이 기능이 정식으로 지원되기를 기다려보는 것이 좋다.
6. FrontEnd 개발에서 ESM, CJS
프로젝트에 모든 파일을 ESM으로 설정하는 법 - package.json
•
브라우저 환경이나 Node.js에서 기본적으로 모든 .js 파일은 CJS 파일로 해석되므로 ESM 파일로 해석 하기위해서는 모든 js 파일을.mjs 확장자로 변경해야한다.
1.
모든 파일명을 .mts, 또는 .mjs로 확장자를 설정한다.
2.
package.json의 type field를 “module”로 설정한다.
{
"type": "module",
}
JSON
복사
•
type 필드가 "module"로 설정된 경우: 프로젝트는 ESM으로 간주되며, .js 파일도 .mjs로 해석된다.
•
type 필드가 "commonjs"로 설정된 경우: 프로젝트는 CJS로 간주되며, .js 파일은 .cjs로 해석된다.
Typescript
{
"compilerOptions": {
// omitted
"allowJs": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2020"
}
// ommited
}
JSON
복사
•
target → 빌드하는 결과물이 어떤환경에서 실행될 것인지 설정하는데 사용되는 필드이다.
•
module → 트랜스파일된 결과물이 host(js 파일이 실행될 시스템을 의미)의 어떤 모듈환경에서 실행될것인지 설정하는데 사용되는 필드이다. 즉, TypeScript 컴파일러가 JavaScript 모듈 시스템을 어떻게 변환할지 설정하는 데 사용된다는 뜻이다. 이는 TypeScript 코드를 작성할 때, 컴파일 후 코드가 실행될 환경과 사용되는 모듈 시스템에 맞도록 변환 방식을 제어한다.
◦
es2015 → import와 export가 명세에 추가됨
◦
es2020 → import.meta export * as ns from “mod”와 같은 문법이 추가됨
◦
es2022 → top-level await이 추가됨 - 추천
•
moduleResolution → TypeScript가 모듈을 어떻게 탐색하고 로드할지 설정하는 옵션이다. 이 설정은 코드에서 import나 require로 선언된 모듈을 TypeScript가 찾는 방식을 제어하며, 컴파일러가 모듈 경로를 해석하는 알고리즘을 정의한다. 설정값에 따라 모듈 탐색이 Node.js 스타일로 이루어질지, 아니면 TypeScript의 고유 방식으로 이루어질지가 결정된다.
◦
classic → TypeScript의 초기 버전에 사용되었던 방식으로, Node.js의 모듈 탐색 규칙과는 다르다. 주로 레거시 코드와의 호환성을 위해 사용된다.
◦
node → Node.js의 모듈 탐색 방식을 따르는 옵션으로, 대부분의 현대 프로젝트에서 기본적으로 사용된다. node_modules 폴더를 기준으로 모듈을 탐색하며, JavaScript 파일 확장자 생략 및 폴더 내 index.js 파일을 자동으로 로드하는 기능을 포함한다.
◦
bundler → 번들러 환경(예: webpack, Vite 등)에 최적화된 방식으로, 번들러가 처리할 수 있도록 모듈을 탐색하고 로드하는 데 유용하다. TypeScript 5.0부터 도입되었다. - FE 개발 시 추천
6. CJS와 ESM의 차이점 및 상관관계
여러번 import 시
•
ESM → 동일한 모듈을 여러 번 import할 경우, 첫 번째를 제외한 나머지 import는 무시되며, 결과적으로 단일 모듈만 로드된다.
•
CJS → 브라우저에서 동일한 모듈을 여러 번 import해도 이전에 import한 결과와 관계없이 다시 실행된다. 그러나 메모리 누수에 대한 걱정은 적다. CJS는 최초 로드 이후 동일한 모듈을 다시 로드할 때 캐싱된 결과를 반환하므로 성능에 큰 영향을 주지 않는다.
Instantiation
•
ESM → Live Binding을 사용하여 동일한 메모리 주소를 참조한다. 이를 통해 값이 변경되면 즉시 반영된다. 단, 객체를 가져온 경우 해당 객체의 속성 값을 수정할 수 있다.
•
CJS → 모듈에서 내보낸 객체는 내보낼 때 복사되므로, 내보내는 값(예: 숫자 등)은 복사본이 된다.
Module specifiers에 변수 사용
// CJS
require(`${path}/index.js`) // valid
// ESM
import { count } from `${path}/counter.js` // invalid
const module = await import(`${path}/counter.js`); // valid
TypeScript
복사
•
ESM → 정적 분석을 기반으로 하므로, module specifier에 변수를 사용할 수 없으며 반드시 문자열 리터럴을 사용해야 한다. 동적 모듈 로드가 필요한 경우 import() 함수를 사용해야 한다.
•
CJS → 런타임에서 문자열 템플릿을 사용해 동적 경로로 모듈을 로드할 수 있다. 이를 통해 모듈의 경로를 동적으로 설정 가능하다.
파일경로
•
ESM → 파일 이름은 import.meta.url로, 디렉터리 이름은 new URL(".", import.meta.url)로 가져올 수 있다.
•
CJS → 파일 이름은 __filename으로, 디렉터리 이름은 __dirname으로 가져올 수 있다.
URL로 모듈 로드
•
ESM → URL을 통해 파일을 import하여 네트워크 리소스를 모듈로 활용하거나 CDN을 통해 의존성을 관리할 수 있다.
•
CJS → URL 기반 모듈 로드는 지원되지 않으며, HTTP 요청 라이브러리를 통해 파일을 다운로드 후 실행해야 한다.
7.1. 결론 1 - 패키지는 ESM과 CJS를 모두 배포하기
ESM또는 CJS 하나로만 빌드 하여 배포하기? - no
•
결과적으로 ESM이 공식적으로 존재하는 유일한 모듈 방식이고 Tree shaking에도 좋은 영향을 끼친다. 그러면 ESM으로만 배포해도 되는 것이 아닌가? 또는 CJS가 더 많이 사용되므로 CJS 빌드 결과물만 배포해도 되는 것이 아닌가? 라고 생각할 수 있지만, 현실적으로는 다양한 환경에서의 호환성을 고려해야 한다. ESM과 CJS를 동시에 지원하는 방식이 일반적으로 요구되며, 이는 사용자와 개발 환경 간의 충돌을 최소화하기 위함이다.
CJS에 ESM Wrapper 작성하기 - no
import cjsModule from '../index.js'
export const foo = cjsModule.foo
JavaScript
복사
•
CJS로 빌드된 .cjs 결과물에 ESM Wrapper를 작성하여 배포하는 방법이다.
•
ESM Wrapper는 CJS 빌드 결과물을 ESM 환경에서 편리하게 사용할 수 있도록 변환 인터페이스만 제공한다. 그러나, 결과적으로 해당 모듈은 CJS로 작동하기 때문에 ESM 기반의 Tree-shaking은 지원되지 않으며, import된 모듈 전체가 로드된다.
ESM과 CJS 모두 배포하기 - yes
cjs와 esm 모두 배포하는 react-hook-form
•
대부분의 비교적 최신 라이브러리들은 가장 많이 사용되는 두 가지 모듈 시스템(CJS와 ESM)을 사용하는 사용자들을 지원하기 위해 두 버전의 결과물을 함께 배포한다.
•
두 가지 빌드 결과물을 생성한 후, package.json에 각각 CJS와 ESM의 엔트리포인트를 명시할 수 있다.
{
"name": "@tanstack/react-query",
// ..omitted
"exports": {
".": {
"import": {
"types": "./build/modern/index.d.ts",
"default": "./build/modern/index.js"
},
"require": {
"types": "./build/modern/index.d.cts",
"default": "./build/modern/index.cjs"
}
},
}
// ..omitted
}
JSON
복사
◦
위 코드는 @tanstack/react-query의 package.json 예시이다.
▪
해당 설정에 따르면, require를 사용하여 패키지를 가져오는 경우 ./build/modern/index.cjs에서 패키지를 로드하며, import를 사용하는 경우 ./build/modern/index.js를 기준으로 로드한다.
7.2. 결론 2 - Node.js 서버 결과물은 cjs로 배포하기
•
위 두 개의 GitHub Issue에서 확인할 수 있듯이, Node.js 환경에서는 CJS가 ESM보다 유의미하게 느리다는 점을 확인할 수 있다.
•
애플리케이션 크기가 커질수록 실행 전에 사용하는 모듈을 파악하고 Module Records 및 Module Environment Records를 생성하는 과정에서 성능 차이가 더욱 두드러질 수 있다. 따라서 Node.js 실행 환경에서는 실행 코드가 CJS로 빌드되는 것이 더 효율적이다.
7.3 결론 3 - FrontEnd 개발 시 ESM으로 개발하기
•
번들 결과물의 Tree shaking 최적화를 위해서는 프론트엔드 개발 시 ESM을 사용하여 번들러가 Tree-shaking을 수행할 수 있도록 해야 한다.