JavaScript 모듈 시스템의 발전 과정을 이어서 살펴보면, AMD와 CommonJS는 각각 브라우저와 서버 측 환경에 최적화된 솔루션을 제공했지만, 두 환경을 모두 지원하는 모듈을 작성하는 데에는 어려움이 있었습니다.
UMD는 이러한 문제를 해결하고자 등장했습니다.
UMD는 AMD와 CommonJS 모듈 시스템을 결합하여, 동일한 모듈이 브라우저 환경과 Node.js 환경에서 모두 동작할 수 있도록 하기 위해 만들어졌습니다.
이를 위해 UMD는 실행 환경을 자동으로 감지하고, 해당 환경에 맞는 모듈 정의 방식을 사용합니다. 이를 통해 모듈의 재사용성과 호환성을 높였습니다.
⇒ 주요 환경을 감지하고 각 환경에 맞는 방식으로 모듈을 정의해서 사용합니다.
define 함수가 정의되어 있는지 확인합니다.define 함수를 사용하여 모듈을 정의합니다.module과 module.exports가 정의되어 있는지 확인합니다.module.exports를 사용하여 모듈을 내보냅니다.1(function (root, factory) {
2 if (typeof define === 'function' && define.amd) {
3 // AMD 환경
4 define([], factory);
5 } else if (typeof module === 'object' && module.exports) {
6 // CommonJS 환경
7 module.exports = factory();
8 } else {
9 // 전역 변수 (브라우저) 환경
10 root.MyModule = factory();
11 }
12}(typeof self !== 'undefined' ? self : this, function () {
13 // 모듈 정의
14 return {
15 sayHello: function() {
16 return 'Hello, world!';
17 }
18 };
19}));장점
하나의 모듈을 작성해 여러 환경에서 사용할 수 있게합니다.
단점
다양한 환경을 지원하기 위해 조건문과 설정이 많아져 코드가 복잡해질 수 있습니다. 환경 감지 순서 - AMD → CJS → 전역 변수 순서로 감지하는 것이 일반적입니다.
환경 감지와 다양한 로딩 방식을 처리하기 위한 추가 코드로 인해, 실행 성능이 약간 저하될 수 있습니다.
각 환경마다 다른 테스트 설정이 필요합니다.
*왜 환경을 감지할 때 AMD → CJS → 전역 변수 순서를 따르는게 일반적인가요?
아쉬운 점은 UMD는 AMD와 CommonJS를 지원하기 위해 복잡한 조건문과 환경 감지가 필요하다는 것입니다.
이때 모듈 시스템의 표준화를 통해 모듈 관리를 개선하고 최적화 하기 위해서 ESM이 등장하게됩니다.
ECMAScript 표준에 따라 JavaScript 모듈을 정의하고 사용하는 방식입니다.
JavaScript 모듈 시스템의 표준으로, 브라우저와 서버 환경 모두에서 사용됩니다.
최신 브라우저와 Node.js에서 네이티브로 지원되기에 추가적인 라이브러리나 도구 없이 모듈을 사용할 수 있습니다.
장점
ESM은 비동기와 동적 로딩을 모두 지원합니다. 기본적으로 모듈을 비동기로 로드하며, 필요에 따라 import() 함수를 사용하여 동적으로 모듈을 로드할 수 있습니다. 이는 클라이언트와 서버 측 환경 모두에서 유연한 모듈 로딩을 가능하게 합니다.
ESM은 간단하고 일관된 문법을 제공합니다. import와 export 문법을 사용하여 모듈을 정의하고 로드할 수 있습니다. 이는 개발자가 모듈 시스템을 쉽게 이해하고 사용할 수 있도록 합니다.
각 모듈이 고유한 네임스페이스를 가져서 전역 변수 충돌을 방지하고 기능별로 모듈을 나누어 작성해 필요에 따라 로드해서 사용할 수 있습니다. → 유지보수성과 협업 효율성 향상
JavaScript 엔진과 번들러(Webpack, Rollup 등)가 정적 분석을 바탕으로 최적화를 수행할 수 있습니다. 이를 통해 트리 셰이킹(Tree Shaking)과 같은 최적화 기법을 쉽게 적용할 수 있습니다.
ESM은 ECMAScript의 표준으로, 모든 현대 브라우저와 Node.js와 같은 서버 측 환경에서 네이티브로 지원됩니다. 이는 추가적인 라이브러리나 설정 없이도 모듈을 사용할 수 있게 합니다.
ESM 모듈은 자동으로 strict mode에서 실행됩니다. 이는 코딩 실수를 줄이고, 보다 안전한 코드를 작성할 수 있게 합니다.
import를 사용한 동적 임포트 예제
1function loadModule() {
2 import('./module.js').then(module => {
3 module.default();
4 });
5}
6
7document.getElementById('loadButton').addEventListener('click', loadModule);*정적 분석이란? 코드 실행 없이 소스 코드를 분석하는 것을 의미합니다. 코드의 구조, 의존성, 사용되지 않는 코드 등을 파악할 수 있습니다.
단점
구형 브라우저에서는 폴리필이나 트랜스파일링이 필요합니다. 이를 위해 Babel과 같은 도구를 사용할 수 있습니다.
Node.js에서 ESM을 사용하려면 package.json에 type:”module” 을 추가하거나 .mjs확장자를 사용해야 합니다. 이는 기존 CommonJS 모듈과 호환성 문제를 일으킬 수 있습니다.
JavaScript 엔진이나 번들러(Webpack, Rollup 등)는 소스 코드를 파싱하여, 각 모듈이 어떤 다른 모듈을 import하는지 분석합니다. 이를 통해 모듈 간의 의존성 그래프를 생성합니다.
모듈 간의 관계를 명확히 보여주며, 어떤 모듈이 어떤 다른 모듈에 의존하는지 알 수 있습니다.
1// moduleA.js
2export const foo = 'foo';
3
4// moduleB.js
5import { foo } from './moduleA.js';
6console.log(foo);위의 코드에서, moduleB.js는 moduleA.js에 의존합니다. 정적 분석을 통해 이 관계가 그래프로 표현됩니다.
//그래프를 시각화한 예시
├── moduleA.js
│ └── moduleC.js
└── moduleB.js
└── moduleC.js정적 분석을 통해 사용되지 않는 코드를 제거하는 최적화 기법입니다. 번들러는 의존성 그래프를 분석하여, 실제로 사용되지 않는 export 항목을 번들에서 제외할 수 있습니다.
이를 통해 번들 크기를 줄이고, 로딩 성능을 최적화할 수 있습니다.
1// moduleA.js
2export const foo = 'foo';
3export const bar = 'bar'; // 사용되지 않음
4
5// moduleB.js
6import { foo } from './moduleA.js';
7console.log(foo);위의 코드에서 bar는 moduleB.js에서 사용되지 않으므로, 트리 셰이킹을 통해 번들에서 제거될 수 있습니다.
정적 분석을 통해 모듈 간의 의존성을 파악한 후, 이를 바탕으로 코드 스플리팅을 수행할 수 있습니다. 이를 통해 애플리케이션의 초기 로딩 성능을 향상시킬 수 있습니다.
자주 사용되는 모듈과 그렇지 않은 모듈을 분리하여, 초기 로딩 시점에 필요한 코드만 로드하고 나머지는 비동기적으로 로드합니다.
1// main.js
2import { foo } from './moduleA.js';
3
4function loadModule() {
5 import('./moduleB.js').then(moduleB => {
6 moduleB.doSomething();
7 });
8}
9
10console.log(foo);위의 코드에서 moduleB.js는 동적 import()를 통해 비동기적으로 로드됩니다. 이를 통해 초기 로딩 성능을 최적화할 수 있습니다.
번들러는 정적 분석을 통해 모듈 간의 관계를 파악하고, 최적의 번들링 전략을 세웁니다. 이를 통해 중복된 코드를 제거하고, 필요한 모듈만 포함하는 최적화된 번들을 생성할 수 있습니다.
1// main.js
2import { foo } from './moduleA.js';
3import { bar } from './moduleC.js';
4
5console.log(foo);
6console.log(bar);위의 코드에서 moduleA.js와 moduleC.js의 의존성을 분석하여, 필요한 모듈만 포함하는 최적화된 번들을 생성합니다.
ESM이 도입된 이후 UMD와 CommonJS 모듈 시스템을 사용하는 상황은?
UMD는 라이브러리 배포와 다양한 환경 지원을 위해, CommonJS는 Node.js환경과 레거시 코드베이스 유지보수를 위해 사용됩니다.
UMD를 사용하다가 ESM으로 마이그레이션 하려면 어떤 것들이 고려될까?
이렇게 모듈 시스템의 변천사를 살펴봤습니다.
각 모듈 시스템들은 이전 방식의 아쉬운 점들을 보완하기 위해 계속 발전해왔습니다.
모듈 시스템의 발전은 UX/DX의 엄청난 상승을 가져왔다고 느꼈는데요.
(CJS가 없었더라면 지금의 Framework들도 없었을 것이고 Node.js가 없었으면 npm이 없었으면.. )
위 경우들처럼 개발생태계에 큰 기여를 하는 생에 꼭 경험해보고싶습니다.