📍 참고
CommonJS와 ESM에 모두 대응하는 라이브러리 개발하기: exports field
CJS 와 ESM
[javascript] 모듈 시스템 - CJS, ESM 동작원리 이해하기
모듈 시스템의 사용은 바퀴를 다시 만들지 않는 것과 같다.
모든 프로그래밍 언어는 실행환경이 필요하다.
자바스크립트 또한 자바스크립트 엔진이 구동한다.
자바스크립트 코드를 실행하는 프로그램 또는 인터프리터.
크롬의 경우 V8 엔진, 파이어폭스는 스파이더몽키
node.js와 브라우저는 JS를 실행하기 위한 자바스크립트 엔진을 내장하고 있다.
서버 개발 환경을 제공하는 것이 주된 목적.
서버 측에서 자바스크립트 코드를 실행할 수 있게 해주고, 이를 통해 파일 시스템, 네트워크 통신, 데이터베이스와의 연동 등을 가능하게 한다
클라이언트 측에서 자바스크립트를 실행하여 동적인 웹 페이지를 구현하는 데 중점
두 환경 모두 자바스크립트 기반이기에 ECMAScript 실행할 수 있지만, 각 환경에서 자신의 특수한 목적 실현시키기 위해 제공되는 API들은 서로 호환이 되지 않는다.
ECMAScript는 ECMA에서 만든 스크립트이다.
📍자바스크립트의 표준 정의 : CommonJS vs ES Modules
에 상세히 설명
자바스크립트는 ES2015 이전까지 모듈을 지원하지 않아 하나의 파일에 모든 코드를 담아야했다.
가독성 저하, 변수 충돌 등의 문제를 해결하기 위해 모듈 시스템이 도입됨.
Node.js에는 CommonJS(CJS), ECMAScript Modules(ESM)라는 두 가지 모듈 시스템이 존재한다.
Node.js에서 가장 일반적으로 사용되는 모듈 시스템.
동적 로딩을 지원하며, 런타임에 require()를 사용하여 모듈을 가져온다
if (condition) {
const moduleA = require('./moduleA'); // 런타임에서 동적 로딩 가능
}
require()module.exports동적으로 처리하기 때문에 조건문 내부에서 처리가 가능하고,
런타임에서만 모듈간 의존관계를 파악할 수 있다.
최신 JavaScript 버전에서 지원되는 모듈 시스템
import moduleA from './moduleA'; // 정적으로 로드됨 (컴파일 시 분석)
importexportESM module loader는 비동기적으로 작동 (Top-level Await을 지원)
따라서 ESM에서 CJS를 import 할 수는 있지만, CJS에서 ESM을 require 할 수는 없다. CJS는 Top-level Await을 지원하지 않기 때문
코드를 가져오기 전 실제 사용하는 부분과 사용하지 않는 부분을 미리 파악한다.
컴파일 타임시 정적으로 처리하기 때문에 조건문 처리가 불가능하다.
따라서 import는 최상단 스코프에서만 사용이 가능하다.
트리 쉐이킹이 왜 필요한가?
브라우저 환경에서는 페이지 렌더링을 빠르게 하는 것이 중요한데, 이 때 JavaScript는 로딩되어 실행되는 동안 페이지 렌더링을 중단시키는 리소스들 중 하나 입니다.
따라서 JavaScript 번들의 사이즈를 줄여서 렌더링이 중단되는 시간을 최소화 하는 것이 중요합니다. 이를 위해 필요한 것이 바로 Tree-shaking입니다. Tree-shaking이란 필요하지 않은 코드와 사용되지 않는 코드를 삭제하여 JavaScript 번들의 크기를 가볍게 만드는 것을 말합니다.
https://toss.tech/article/commonjs-esm-exports-field
CJS는 Tree-shaking이 어렵고, ESM은 쉽게 가능하다.
CJS는 동적인 구조로 require/module.export 한다.
// https://toss.tech/article/commonjs-esm-exports-field 코드 참조
// require
const utilName = /* 동적인 값 */
const util = require(`./utils/${utilName}`);
// module.exports
function foo() {
if (/* 동적인 조건 */) {
module.exports = /* ... */;
}
}
foo();
따라서 빌드 타임에 정적 분석을 적용하기가 어렵고,
런타임에서만 모듈 관계를 파악할 수 있다.
ESM은 정적인 구조로 모듈끼리 의존하도록 강제한다.
import path에 동적인 값을 사용할 수 없고, export는 항상 최상위 스코프에서만 사용할 수 있다.
// https://toss.tech/article/commonjs-esm-exports-field 코드 참조
import util from `./utils/${utilName}.js`; // 불가능
import { add } from "./utils/math.js"; // 가능
function foo() {
export const value = "foo"; // 불가능
}
export const value = "foo"; // 가능
따라서 ESM은 빌드 단계에서 정적 분석을 통해 모듈 간의 의존 관계를 파악할 수 있다.
CJS 환경에서는 ESM 모듈을 사용할 수 없지만,
ESM 환경에서도 CJS를 실행할 수 있다.
브라우저는 ESM 시스템을 사용하니 CJS 모듈은 다 실행할 수 있어야 할텐데, 왜 문제가 될까?
브라우저의 정적 분석(Static Analysis)
브라우저는 네트워크 환경에서 동작하기 때문에 최적화된 로딩 방식을 필요로 한다. 정적 분석을 통해 필요한 모듈을 미리 파악하면 병렬 다운로드가 가능하다.
Node.js는
CJS(CommonJS)와 ESM(ECMAScript Modules) 모두 사용 가능하지만,
브라우저는 ESM만 지원하며 정적 분석이 필요한 환경이다.
컴파일 타임에 모듈을 정적으로 로드하는 ESM는 호환되지만,
런타임에 모듈을 동적으로 로드하는 CJS는 정적분석이 불가능해서 호환되지 않는다.
브라우저에서 CJS 모듈을 사용하려면 번들링(Webpack, Vite 등)을 통해 변환이 필요하다.
node.js는 서버측에서 동작하는 환경이기 때문에 브라우저에서 사용할 수 없는 특정 API나 모듈을 제공한다.
파일 시스템 관련 모듈 fs, 네트워크 관련 모듈 http, https 과 같은 모듈은
브라우저에서 사용할 수 없다.
package.json의 type field 또는 확장자를 보고 알 수 있다.
.js파일의 Module System은 package.json의 type field에 따라 결정됩니다.
type field의 기본값은"commonjs"이고, 이 때 .js 는 CJS로 해석됩니다.
다른 하나는"module"입니다. 이 때 .js 는 ESM으로 해석됩니다.
.cjs 는 항상 CJS로 해석됩니다.
.mjs 는 항상 ESM으로 해석됩니다.
https://toss.tech/article/commonjs-esm-exports-field
| CJS | ESM | |
|---|---|---|
| 모듈 사용 | require() | import |
| 모듈 내보내기 | module.export | export |
| 모듈 로드 시점 | 런타임 | 컴파일 타임 |
| 모듈 로딩 | 동적(조건문 내부 가능) | 정적(최상위에서만 가능) |
| node.js | 브라우저 | |
|---|---|---|
| 모듈시스템 | CJS | ESM |
| 적합한 환경 | 서버 측 개발 | 클라이언트 측 개발 |