자바스크립트는 웹 개발의 핵심 언어 중 하나로서, 자바스크립트로 개발을 하다보면 CJS라 불리는 CommonJS, 그리고 ESM 이라 불리는 ECMAScript이라는 두가지 모듈 시스템을 마주치게 된다. 자연스럽게 "왜 같은 자바스크립트인데 두 개의 모듈 시스템이 존재할까?"라는 궁금증이 들기 마련이다. 이 글에서는 모듈 시스템이 어떻게 탄생하게 되었고, 어떤 특징과 차이점을 가지고 있는지 알아보자.
웹 초기 시절에 JS는 브라우저에서 간단한 동작만을 위해 존재했기 때문에 모듈(애플리케이션을 구성하는 개별적 요소로서 재사용 가능한 코드 조각) 시스템이 필요하지 않았다. HTML에 script 태그로 간단히 적을 수 있는 정도였다. 하지만 웹앱의 기능이 커지면서 복잡하고 긴 코드를 좀 더 정리하고 재사용할 수 있도록 하는 모듈 시스템의 필요성이 대두되었다.
CJS 모듈 시스템을 도입했다고 말할 수 있는 것은 구글의 V8 엔진을 사용해서 만든 Node.js 였고, 이후 2015년 ES6에서 공식적인 JS의 모듈 시스템이 발표되면서 이 두 모듈 시스템의 공존이 시작되었다.
ECMAScript Modules (ESM)
간략한 소개
- ESM은 ECMAScript 2015 (ES6)에서 소개된 JavaScript의 공식 표준이다.
import
,export
문을 사용한다.- package.json에
"type": "module"
이 기술되어 있으면.js
도 사용가능하지만, 주로.mjs
확장자를 사용한다.
특징
- 비동기 로딩 (Asynchronous Loading)
ESM은 비동기 로딩을 지원한다. 모듈을 비동기적으로 로딩한다는 것은 프로그램 실행 중 필요한 모듈을 불러오되, 그 로딩 과정이 메인 프로그램의 실행을 차단하지 않고 독립적으로 진행된다는 의미이다. 비동기 모듈 로딩은 특히 네트워크 리소스를 활용할 때 유용하게 쓰인다.-▷ 이 특성의 장점으로 Code Splitting을 예로 들 수 있다.
- 정적 구조 (Static Structure)
ESM의 import
는 정적으로 분석 가능하다. 이는 프로그램이 실행되기 전에 필요한 모듈이나 라이브러리를 코드의 상단에 선언하고, 프로그램 실행 시에 모든 의존성을 사전에 해석하고 로드하는 방식을 의미한다.
정적 로딩의 특징
- 코드의 명시성: 모듈 로드를 위해
import
와export
문을 사용하여, 모듈 간의 의존성을 파일 상단에서 선언적으로 표현한다. 이로 인해 어떤 모듈이 어떤 다른 모듈에 의존하는지 코드를 통해 쉽게 파악할 수 있다. - 빌드 시점에서의 의존성 해석: 정적 로딩 방식에서는 트랜스파일링 또는 빌드 과정 중에, import / export 문을 해석하여 모듈 간의 의존성을 파악한다. 이는 불필요한 코드를 제거하는 트리 쉐이킹과 같은 기술을 통해 필요한 모듈만을 포함하도록 최적화 작업을 수행할 수 있다.
정적 로딩의 장점:
- 최적화 용이: 빌드 도구가 코드 분석을 통해 불필요한 모듈을 제거하거나, 필요한 모듈만을 번들링하여 성능을 최적화 할 수 있다.
- 에러 발견 용이: 의존성 문제나 임포트 오류 등을 개발 과정에서 빠르게 발견할 수 있다.
정적 로딩의 단점:
- 유연성 제한: 모든 의존성을 코드 상단에 명시해야 하므로, 동적 로딩에 비해 유연성이 떨어질 수 있다.
이는 코드가 실행되기 전에 종속성이 해결되고 모듈 그래프가 구성된다는 말이다. 즉, 최적화가 향상되고 트리 쉐이킹(번들링 프로세스 동안 사용하지 않는 코드 제거하는 작업)이 가능하다.
// 모듈에서 // math.mjs (export 된 함수 중 일부만 사용)
export function add(a, b) {
return a + b;
}
export function unusedFunction() {
return 'This will be removed during thee shaking.';
}
// 실제 사용 부분
// app.mjs (import된 함수만 사용)
import { add } from './math.mjs';
add(2,3); // 5
위 코드를 빌드하면 사용하지 않은 unusedFunction은 제거되고 번들 파일이 구성된다.
CommonJS (CJS)
간략한 소개
- CJS는 ESM 보다 오래된 모듈 시스템이며, Node.js에서 주로 쓰인다.
- 모듈을 가져오기 위해
require()
, 그리고module.exports
가 사용된다. - package.json에
"type": "commonjs"
이 기술되어 있으면 .js도 사용가능하지만, 주로 .cjs 확장자를 사용한다.
특징
- 동기적 로딩
모듈을 동기적으로 로딩한다는 것은 프로그램이 실행될 때 필요한 모듈이나 라이브러리를 즉시 로드하고, 해당 모듈이 완전히 로드되고 초기화될 때까지 프로그램 실행을 일시 중지하는 방식을 말한다. 이 방식은 특히 서버 사이드 환경에서 많이 사용된다.
동기적 로딩은 다음과 같이 이루어진다.
- 실행 중지: 모듈을 로드하는
require()
함수가 호출되면, Node.js는 해당 모듈의 코드를 파일 시스템에서 찾아 메모리에 로드한다. 이 과정에서 프로그램의 다른 실행이 멈춘다. - 코드 실행: 모듈이 성공적으로 로드되면, 그 모듈의 코드가 실행된다. 모듈 내의 코드가 완료되어야만 원래의 프로그램 실행이 계속된다.
- 결과 반환: 로드된 모듈에서 필요한 함수나 객체를 반환하고, 이를 사용하여 프로그램의 나머지 부분이 실행된다.
// module1.js
console.log("module1 로드 시작");
setTimeout(() => { console.log("module1 실행"); }, 2000);
console.log("module1");
// module2.js
console.log("module2 로드 시작");
setTimeout(() => { console.log("module2 실행"); }, 2000);
console.log("module2");
// index.js
console.log("시작");
const module1 = require("./module1");
console.log("index!");
const module2 = require("./module2");
console.log("종료");
index.js를 실행하면? 결과는 아래와 같다.
시작
module1 로드 시작
module1
index!
module2 로드 시작
module2
종료
module1 실행
module2 실행
동기적 로딩의 장점
- 단순성: 코드 흐름이 단순하고 예측 가능하다. 디버깅이 비교적 쉬워 개발 초기 단계에서 유용할 수 있다.
- 명확한 의존성 관리: 필요한 모듈이 명확히 로드되므로, 의존성 관리가 간단해진다.
동기적 로딩의 단점
- 성능 문제: 모든 모듈 로딩이 완료될 때까지 실행이 중지되므로, 특히 대용량 파일이나 네트워크를 통해 로드해야 하는 자원의 경우 응답 시간이 길어질 수 있다.
- 스케일링 제한: 대규모 앱에서 여러 모듈을 동기적으로 로드하면 성능 저하가 발생할 수 있다.
- 동적 구조 (Dynamic Structure)
CJS의 require() 문에서 변수를 사용할 수 있으며 동적으로 모듈을 로드하는 것을 허용하여 유연성을 제공한다. 그러나 ESM에 비하면 최적화 면에서든 덜 효율적이다. 아래 예에서 loadModule 함수는 주어진 조건에 따라 다른 모듈을 로드하고 해당 모듈의 기능을 실행한다. 이렇게 동적으로 모듈을 관리함으로써 필요할 때에만 자원을 사용하게 된다.
function loadModule(condition) {
if (condition) {
const moduleA = require('./moduleA');
mobuleA.doSomething();
} else {
const moduleB = require('./moduleB');
moduleB.doSomethingElse();
}
}
동적 로딩은 유연성과 자원 관리 측면에서 많은 이점을 제공하지만, 빌드 시점에 모든 의존성을 파악하기 어렵기 때문에 최적화와 분석이 더 복잡해질 수 있다.
Node.js 에서의 코드 스플리팅?!
Node.js 서버는 일반적으로 시작 시 모든 코드를 메모리에 로드한다. 서버가 가동 중일 때는 모든 기능이 메모리에 상주하기 때문에, 클라이언트 사이드와 같은 코드 스플리팅으로 인한 초기 로딩 시간 단축 효과는 존재하지 않는다.
하지만 특정 조건 아래에서만 필요한 기능의 모듈을 동적으로 로딩하는 방식으로 코드 스플리팅을 구현할 수 있다. 예를 들어, 특정 API 요청이 들어왔을 때만 특정 모듈을 로드하여 처리하는 식이다. 위에서와 같이 require() 를 조건적으로 사용하는 방식으로 구현할 수 있다.
지금 두 모듈 시스템은 여러 요인들에 따라 결국 공식적인 표준인 ESM이 우위를 차지하는 추세인것 같다. 현재는 Node.js 최신 버전에서는 ESM이 공식적으로 사용되고 있어서 현 직장에서 백엔드 개발자들도 import / export를 이용하여 개발을 진행하고 있다. 그러나 지금까지 수많은 라이브러리들이 CJS로 작성되었기에, 그리고 CJS 로 작성된 기존 코드와 라이브러리를 효과적으로 활용하고 통합하는 방법을 알아야 하기에, 이 모듈의 동작방식을 이해하는 것과 더불어 두 모듈 시스템 간의 호환성 및 자이점을 이해하는 것은 여전히 중요하다.
References
자바스크립트의 표준 정의 : CommonJS vs ES Modules -https://medium.com/@hong009319/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%ED%91%9C%EC%A4%80-%EC%A0%95%EC%9D%98-commonjs-vs-es-modules-306e5f0a74b1
1부) commonjs란 무엇인가? - https://yceffort.kr/2023/05/what-is-commonjs#moduleexports%EB%A1%9C%EB%A7%8C-export%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%98%EB%8B%A4
CJS의 ESM 적용과 동작원리에 기반한 트리쉐이킹 효율성 이해하기 - https://velog.io/@pengoose_dev/CJS%EC%99%80-ESM
트리 쉐이킹으로 자바스크립트 페이로드 줄이기 - https://ui.toast.com/weekly-pick/ko_20180716
ES modules: A cartoon deep-dive - https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
'JS' 카테고리의 다른 글
find() - Lodash 유틸함수 직접 구현 | TypeScript (0) | 2024.06.09 |
---|---|
요즘은 왜 jQuery 를 안쓰는가 ? (0) | 2024.05.14 |
변수 - JS Deep Dive 와 ECMAScript 참고 (0) | 2024.05.06 |