- Published on
esm과 commonJS 이해: 너희 왜그러는데..
- Authors
- Name
- sulmo
자바스크립트 모듈에 관련한 학습 자료들을 보다보니 히스토리가 아직은 중요하다고 생각했다.
단순히 지금 2024년에 개발하는 입장으로써 commonjs와 esm 둘에대한 호환과 사용법을 인지하고 싶은게 다였지만, 과거에대한 간략한 이해와 현재에대한 정리를 진행하면 좋겠다.
자바스크립트 모듈 시스템의 간단 히스토리
HTML의 스크립트로써 단일 파일로 사용되던 JS는 스콥관리 측면에서 불편함이 있었다. 모든 스크립트들이 전역 스콥으로 사용되어 편법으로 분리하며, jQuery가 플러그인으로 붙어 근근히 동작하곤 했다.
Common JS
Node.js가 출시하며, 서버측에서 JS가 구동하게 되었고, 이때 CommonJS 모듈 시스템이 등장한다. 동기적 모듈 로딩 방식을 채택함으로써 서버 환경에서의 직관적인 코드 실행 흐름을 보장했다. 브라우저 환경에서 네트워크 지연 문제로 Webpack과 같은 번들러가 필수 기술 스택으로 부상.
AMD (RequireJS)
CommonJS의 단점을 보완하기위해 AMD(RequireJS 로더)가 비동기와 브라우저 지원을 목적으로 출시 됨. Angular 초기 버전에서 AMD + RequireJS 조합이 동적로딩으로 의존성 관리를 쉽게 할 수 있어 인기였음.
UMD
UMD는 실행 환경을 자동으로 감지해서 적절한 모듈 로딩 방식을 선택하는 방식이다. 한 소스 코드로 브라우저환경에서의 AMD와 node환경의 commonJS를 둘다 지원하고 싶었다. 그래서 보통 라이브러리 제작 시 많이 채택되었다.
- Node.js (CommonJS 환경) →
module.exports
사용 - 브라우저 (AMD 환경) →
define()
사용 - 그 외 (전역 변수) →
window
객체 사용 와 같은 형태로 진행 함.
ESM
위와 같은 과도기 시기를 보내고 최종적으로 JS 모듈 생태계의 표준화 시도 및 성공을 이룬 모듈 시스템이다.(ECMA6 에 등장.) 최신 브라우저 대부분이 지원하며 nodeJS 또한 지원하고 있다. 이제는 어떤 환경이든 ESM을 채택하며, 아직 남아있는 CommonJS를 점진적으로 제거해가는 추세이다.
결론
아직 우리는 commonJS와의 악연을 어느정도 함께하고 있다. 어느정도 차이점을 인지하고 삽질을 줄여야하며, 라이브러리를 채택할때도, 번들러도 모듈 시스템을 아직 모듈의 성향을 어느정도 타는 부분이있다. 이를 인지하고 추가적으로 해결이 안되면 동작방식도 한번은 뜯어보자.
ESM과 CommonJS의 주요 특징
CommonJS
- 동적(require/export): 모듈을 실행할 때
require()
를 호출해야 함. - 동기적 로딩: 모듈을 동기적으로 로드하며,
require()
호출 시 즉시 실행됨. - 트리 셰이킹 불가능: 정적으로 분석되지 않기 때문에 사용되지 않는 코드도 포함됨.
- Node.js 기본 모듈 시스템:
require()
와module.exports
를 사용하여 모듈을 가져오고 내보냄. - 파일 확장자
.cjs
필요 (ESM과 혼용할 때):"type": "module"
을 설정한 경우에도 CJS 파일은.cjs
확장자를 사용해야 함.
ESM
- 정적(import/export): 모듈이 로드될 때
import
와export
가 정적으로 분석됨 → 실행 전에 종속성이 결정됨. - 비동기 로딩 지원:
import()
를 사용하면 동적으로 모듈을 로드 가능. - Top-level await 지원:
await
를 최상위에서 사용할 수 있음. - 트리 셰이킹(Tree-shaking) 가능: 사용하지 않는 코드를 제거하여 번들 크기를 줄일 수 있음.
- 브라우저와 Node.js에서 기본적으로 지원됨:
<script type="module">
을 사용하면 브라우저에서도 직접 실행 가능. - 파일 확장자 요구 (
.mjs
orpackage.json
설정 필요):- Node.js에서 ESM을 사용하려면
.mjs
확장자를 사용하거나,package.json
에서"type": "module"
을 설정해야 함
- Node.js에서 ESM을 사용하려면
상세 동작 방식을 이해하기 위해 (이 문서)[https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/]를 한번 정독해볼 필요가 있다.
비교 정리
특징 | ESM (ECMAScript Modules) | CommonJS (CJS) |
---|---|---|
선언 방식 | import/export | require/module.exports |
로딩 방식 | 정적 (compile-time) | 동적 (runtime) |
실행 방식 | 비동기 가능 (Top-level await 지원) | 동기적 실행 |
트리 셰이킹 | ✅ 가능 | ❌ 불가능 |
브라우저 지원 | ✅ 기본 지원 (<script type="module"> ) | ❌ 직접 실행 불가 (번들링 필요) |
Node.js 지원 | ✅ (.mjs 또는 package.json 설정 필요) | ✅ 기본 지원 |
확장자 | .mjs (또는 "type": "module" ) | .cjs (또는 기본 .js ) |
Top-levle await란..?
ES 모듈(
.mjs
파일 또는"type": "module"
이 설정된 환경)에서는 async 내부가 아닌 최상위에서도await
를 직접 사용할 수 있다.
🔥 주의
await
가 모듈 로딩을 차단할 수 있으므로, 여러 모듈 간 의존성을 고려하여 사용
// module-a.mjs
export const data = await fetch('https://api.example.com/data').then((res) => res.json())
// module-b.mjs
import { data } from './module-a.mjs'
console.log(data) // module-a가 완료될 때까지 module-b의 실행이 지연됨!
마무리
간단하게 히스토리를 살펴보면서 매번 대충 넘긴 과거 모듈들을 간단히 정리해보았다. 모노레포에서 패키지의존 관련하여 파일 로딩 시, 울어대는 원인을 조금은 더 완만하게 합의 볼 수 있을 것 같다. commonJS는 구형 라이브러리나 일부 과거 프로젝트에서 잔재할 수 있음으로 이 부분을 인지하고 ESM으로 고쳐나가자..