핵심 요약
| 구분 | 내용 |
|---|---|
| 모듈 시스템 | 코드 구조화, 재사용성, 유지보수성 향상 |
| CommonJS | Node.js 서버 환경 모듈 시스템 |
| ESModule | JS 표준 모듈 시스템, 브라우저 환경 지원 |
4.1 자바스크립트 모듈화의 역사
4.1.1 자바스크립트 모듈화의 배경
자바스크립트 모듈화에 큰 영향을 미친 세 가지 사건:
4.1.1.1 1997년 ECMAScript 표준 제정
- 브라우저마다 다른 스크립트 언어(JS, JScript 등) → 호환성 문제
- ECMA에서 ECMAScript 표준 제정 → 모든 브라우저가 동일한 표준 따름
4.1.1.2 1999년 Ajax의 등장
- 이전: 페이지 변화 시 전체 HTML 다시 로드 → 화면 깜빡임
- Ajax: 비동기 데이터 교환 → 필요한 데이터만 요청 → SPA 탄생 촉발
4.1.1.3 2008년 V8 엔진 등장
- 2005년 구글 맵스 → JS의 웹 앱 개발 가능성 입증
- 2008년 V8 엔진 → JS 속도 대폭 개선 → Node.js 등장 → 모듈 시스템 표준화 기여
4.1.2 모듈화 이전의 자바스크립트
초기 JS는 모듈 시스템 없이 파일을 순서대로 로드하는 방식 → 다양한 문제 발생:
| 문제 | 설명 |
|---|---|
| 전역 변수 충돌 | 동일 변수명이 기존 변수 덮어씀 |
| 의존성 관리 | 올바른 순서로 파일 로드 필요 |
| 렌더링 지연 | 스크립트 로드 중 렌더링 블로킹 → 흰 화면 |
4.1.3 자바스크립트 모듈의 여러 시도들
4.1.3.1 즉시 호출 함수 표현식 (IIFE)
IIFE(Immediately Invoked Function Expression): 정의 즉시 실행되는 함수로 모듈 시스템을 흉내냄
(function () {
// 모듈 코드
})();- 즉시 실행 → 새로운 스코프 형성
- 내부 변수/함수는 외부에서 접근 불가 → 네임스페이스 충돌 방지
celsius(셀시우스): 섭씨 온도 / fahrenheit(패런하이트): 화씨 온도
var waterTemperature = (function () {
var boilingPoint = 100;
function convertFahrenheitToCelsius(fahrenheit) {
var celsius = ((fahrenheit - 32) * 5) / 9;
return celsius;
}
return {
units: ["Celsius", "Fahrenheit"],
isWaterBoiling: function (temperature, unit) {
const celsius = unit === "Fahrenheit" ? convertFahrenheitToCelsius(temperature) : temperature;
if (celsius >= boilingPoint) {
return "물이 끓는 중";
}
return "물이 끓지 않음";
},
};
})();접근 가능 여부:
boilingPoint,convertFahrenheitToCelsius()→ 내부 전용 (외부 접근 불가)units,isWaterBoiling()→ 반환 객체 속성 (외부 접근 가능)
console.log(waterTemperature.boilingPoint); // undefined
console.log(waterTemperature.convertFahrenheitToCelsius(72)); // Uncaught TypeError:waterTemperature.convertFahrenheitToCelsius is not a function
console.log(waterTemperature.units); // 접근가능
console.log(waterTemperature.isWaterBoiling(50, waterTemperature.units[0])); // 접근가능IIFE 패턴의 한계
| 한계 | 설명 |
|---|---|
| 네임스페이스 충돌 | 전역에 모듈 선언 → 동일 이름 중복 시 오류 |
| 의존성 관리 어려움 | 모듈 로드 순서 수동 관리 필요 |
const ModuleA = (function () {
return { greet: () => console.log("Hello from ModuleA") };
})();
// 동일한 이름으로 모듈 재선언 시 오류
const ModuleA = (function () {
/* ... */
})();
// Uncaught SyntaxError: Identifier 'ModuleA' has already been declared4.1.3.2 AMD와 RequireJS
AMD(Asynchronous Module Definition): 비동기 모듈 로드 표준, RequireJS가 대표 구현체
특징:
- 비동기 로드 → 병렬 로드 가능 → 초기 로딩 속도 향상
- 의존성 명시적 정의 → 필요한 모듈 로드 후 실행
// 모듈 정의 (define)
define(["dep1", "dep2"], function (dep1, dep2) {
return {
/* 모듈 내용 */
};
});
// 모듈 사용 (require)
require(["myModule"], function (myModule) {
myModule.doSomething();
});문법 비교: AMD vs CommonJS vs ESModule
// AMD - 복잡한 콜백 구조
define(["moduleA"], function (moduleA) {
moduleA.doSomething();
});
// CommonJS - require 함수 사용
const moduleA = require("moduleA");
moduleA.doSomething();
// ESModule - 가장 간결하고 직관적
import { doSomething } from "moduleA";
doSomething();AMD의 단점
| 단점 | 설명 |
|---|---|
| 복잡한 문법 | 콜백 + 배열 의존성 선언 → 가독성 저하 |
| 정적 분석 어려움 | 동적 함수 호출 → 트리셰이킹 적용 어려움 |
| 네트워크 의존 | 비동기 로드 → 네트워크 지연 영향 |
| 서버 환경 제약 | 브라우저 중심 설계 → Node.js 사용 불편 |
AMD/RequireJS는 ESModule 표준화 이후 사용 빈도가 줄어들고 있다.
4.1.3.3 CommonJS
브라우저 외 환경(서버)을 위한 모듈 시스템 → Node.js가 채택
특징:
- 동기적 모듈 로드 → 서버 환경의 I/O 작업에 적합
exports로 내보내고require()로 가져옴
// 내보내기
exports.sum = (a, b) => a + b;
// 가져오기
const { sum } = require("./sum.js");
console.log(sum(1, 2)); // 34.1.3.4 UMD
UMD(Universal Module Definition): CommonJS + AMD + 브라우저 전역 객체를 모두 지원하는 패턴
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["dep"], factory); // AMD
} else if (typeof module === "object") {
module.exports = factory(require("dep")); // CommonJS
} else {
root.myModule = factory(root.dep); // 브라우저 전역
}
})(this, function (dep) {
return {
/* API */
};
});UMD의 단점
| 단점 | 설명 |
|---|---|
| 코드 증가 | 호환성 조건문으로 파일 크기 증가 |
| 성능 저하 | 사용하지 않는 환경 로직도 평가됨 |
| 구식 패턴 | ESModule 표준화로 호환 문제 해소 |
| 복잡한 유지보수 | 조건부 로직으로 가독성 저하 |
현대 JS 환경에서는 ESModule이 UMD를 대체하고 있다.
4.1.3.5 SystemJS
동적 모듈 로더 - 런타임에 모듈을 비동기로 로드하고 의존성 해결
특징:
- 동적 로딩 → 필요 시점에 모듈 로드 → 성능 최적화
- 다양한 형식 지원: AMD, CommonJS, UMD, ESModule
- 구형 브라우저(IE11 등) ESModule 폴리필로 활용
System.register(["dep1"], function (_export, _context) {
return {
setters: [function (dep1) {}], // 의존성 로드 후 실행
execute: function () {
// 모듈 코드 실행
_export({ myValue: 42 });
},
};
});SystemJS의 단점
| 단점 | 설명 |
|---|---|
| 추가 라이브러리 필요 | 별도 로더 의존성 증가 |
| 성능 이슈 | 동적 로딩으로 지연 발생 가능 |
| 복잡한 문법 | ESModule 대비 가독성 저하 |
| 사용성 감소 | ESModule 표준화로 필요성 감소 |
구형 브라우저 지원이나 복잡한 동적 로딩이 필요한 프로젝트에 한해 사용
4.1.3.6 ESModule
ES6 표준 모듈 시스템 - 브라우저와 Node.js 모두에서 공식 지원
등장 배경:
- 이전 모듈 시스템들은 비표준 → 이식성/호환성 문제
- 브라우저와 서버 간 모듈 시스템 불일치 해소 필요
특징:
export/import키워드로 모듈 내보내기/가져오기- 정적 분석 가능 → 트리셰이킹 지원
- 현대 JS 생태계의 표준
4.1.4 오늘날의 자바스크립트 모듈 시스템
서버사이드에서는 CommonJS를, 클라이언트 사이드에서는 ESModule을 사용해 코드를 구성하는 것이 일반적이다. 이 두 모듈 시스템의 특징을 이해하면 자바스크립트 생태계에서 모듈 시스템의 구성 방식을 깊이 있게 파악할 수 있다.
4.1.5 정리
모듈 시스템의 발전 과정:
IIFE → AMD/RequireJS(브라우저 비동기) + CommonJS(서버 동기) → UMD/SystemJS → ESModule
4.2 CommonJS란 무엇일까?
4.2.1 CommonJS의 탄생
2009년 1월, 모질라의 케빈 당구르(Kevin Dangoor)는 자바스크립트를 서버에서 확장성 있게 활용하기 위한 표준을 고민하며 다음 요구사항을 제안했다:
- 상호 호환되는 표준 라이브러리
- 서버와 웹 간 상호작용을 위한 표준 인터페이스
- 다른 모듈을 로드할 수 있는 표준
- 코드 패키지화, 배포 및 설치 방법
- 패키지 설치와 의존성 해결을 위한 패키지 저장소
케빈은 이를 실현하기 위해 ServerJS 그룹을 창설했고, 한 달 만에 200명 이상의 개발자가 참여해 모듈 시스템 표준 명세를 수립했다. 같은 해 9월, 워싱턴 자바스크립트 콘퍼런스에서 CommonJS API 1.0 명세가 발표되었다.
본래 ServerJS라는 이름이었으나, 데스크톱 애플리케이션 등 다양한 환경을 지원하기 위해 CommonJS로 개명했다.
4.2.2 CommonJS의 명세
CommonJS 모듈 시스템의 세 가지 핵심 명세:
1. 독립적인 실행 영역
모든 모듈은 자신만의 독립적인 실행 영역(스코프)을 갖는다. 동일한 변수명을 사용해도 다른 모듈에 영향을 주지 않는다.
// a.js
var a = 1,
b = 2;
console.log(a, b); // 1 2
// b.js
var a = 3,
b = 4;
console.log(a, b); // 3 4
// → 서로 영향을 미치지 않음2. exports 객체로 모듈 정의
외부에 공개할 기능만 exports 객체로 정의한다.
exports.sum = function (a, b) {
return a + b;
};3. require() 함수로 모듈 사용
require() 함수로 필요한 모듈을 불러온다. 모듈명을 전달하면 해당 모듈의 exports 객체가 반환된다.
const { sum } = require("./sum.js");
console.log(sum(1, 2)); // 3브라우저 환경에서의 문제점
| 문제 | 원인 |
|---|---|
| 화면 멈춤 | CommonJS는 동기 로딩 방식으로, 서버의 로컬 디스크 환경을 전제로 설계됨 |
| 전역 변수 충돌 | 브라우저는 파일 스코프가 존재하지 않음 |
| 해결책 | 번들러(webpack, rollup 등) 사용 필수 |
4.2.3 Node.js의 CommonJS
Node.js가 CommonJS를 도입한 이유:
- 다양한 기능의 필요성: 파일 시스템 접근, 네트워크 통신 등 모듈 시스템이 필수
- 논블로킹 I/O 모델과의 호환: 동기적 로딩 방식이 모듈 단위 빠른 로딩에 적합
- CommonJS의 높은 보급도
4.2.3.1 CommonJS 파일 규칙
Node.js가 파일을 CommonJS 모듈로 인식하는 조건:
| 조건 | 설명 |
|---|---|
require() 사용 | .js 파일에서 require() 사용 시 |
package.json 설정 | type: "commonjs" 하위의 .js 파일 |
.cjs 확장자 | .cjs 확장자 파일 |
.mjs파일은 ESModule로 해석되며,require()함수와 호환되지 않는다.
4.2.3.2 모듈 내보내기
module.exports vs exports
require()함수는module.exports를 반환exports단독 사용 시 동일한 결과 반환- 함께 사용 시
module.exports값만 반환
모듈 래퍼와 모듈 스코프
Node.js는 모듈마다 실행 영역을 제공하기 위해 모듈 래퍼 함수로 코드를 감싼다. 이 스코프를 모듈 스코프라고 한다.
자동으로 제공되는 모듈 스코프 변수:
module,exports,require,__filename,__dirname
4.2.3.3 모듈 가져오기
파일 모듈 탐색 순서:
- 절대 경로로 모듈 탐색
- 호출한 파일과 같은 디렉터리에서 탐색
- 상위 디렉터리에서 탐색
- 코어 모듈 또는
node_modules폴더에서 탐색
폴더를 require() 인수로 사용할 경우:
package.json의main속성에 정의된 파일main속성이 없으면index.js파일- 둘 다 없으면
MODULE_NOT_FOUND에러
4.2.3.4 동기적으로 실행되는 require() 함수
4.2.3.5 require.cache
4.2.3.6 순환 참조
4.2.4 소스코드를 CommonJS로 빌드하기
번들링: 의존 관계에 있는 자바스크립트 파일들을 압축, 난독화하여 하나의 파일로 통합하는 작업
트리 셰이킹: 사용되지 않는 모듈을 제거하여 불필요한 코드를 삭제하는 기술
대표 번들러: webpack, parcel, rollup, vite
4.2.4.1 모듈 래퍼는 클로저를 생성
클로저란? 자신이 선언된 환경을 기억해서 내부 함수에서 외부 변수를 사용할 수 있게 하는 메커니즘
| 특성 | 서버 환경 | 브라우저 환경 |
|---|---|---|
| 모듈 위치 | 로컬 디스크 | 네트워크 |
| 클로저 비용 | 큰 문제 없음 | 렌더링 지연, 성능 저하 |
번들러의 해결 방식: 모듈을 하나의 클로저로 통합하여 require() 호출로 인한 성능 문제 감소
4.2.4.2 require()는 런타임에 모듈이 결정된다
모든 require() 호출을 단일 클로저로 연결하면 클로저 성능 문제는 해결되지만, 런타임에 모듈이 결정되므로 정적 분석이 어려워 트리 셰이킹이 어렵다는 단점이 있다
4.2.4.3 트리 셰이킹에 대한 오해
4.2.5 정리
- 모듈시스템의 역사와 특징
- Node.js와의 관계 -> 서버사이드 환경에서 모듈을 효율적 관리 -> 전체적으로 CommonJS명세를 따르지만 module.exports와 같은 Node.js 고유의 확장기능을 갖게됨
- 동기적 로딩방식과 명확한 모듈 정의 및 의존성 해결을 특징으로함
- 동기적 특성은 브라우저 환경에서 한계를 드러내며 클라이언트 사이드에서 비효율적인 동작을 유발
4.3 ESModule이란 무엇일까?
- 자바스크립트 공식적인 모듈 시스템
4.3.1 ESModule의 탄생 배경과 도입
브라우저 환경에서 CommonJS 문제점
-
동기적 로딩방식 : 브라우저에서는 로딩중블로킹이 발생하여 성능저하 초래
-
프리로딩 불가
-
트리셰이킹 및 최적화 어려움 : 모듈을 동적으로 로드하기때문
-
메모리 이슈 : 모듈래퍼 -> 클로저 -> 메모리
-
브라우저 호환성 : 브라우저에서 직접 사용할 수 없음
4.3.2 ESModule의 특징
4.3.2.1 ESModule의 명세
-
export 모듈에서 공개할 변수나 함수 클래스를 명시하는 키워드 이름 내보내기와 기본 내보내기가 있음 모듈 하나에서 이름으로 내보내기는 여러개 존재할수있지만 기본 내보내기는 반드시 하나만 존재해야함
-
import 다른 모듈에서 내보낸 변수나 함수 클래스를 가져오는데 사용된다. import를 사용해서 가져온 모듈은 모듈의 단일 인스턴스를 가져오므로 같은 모듈을 여러번 가져와도 해당 모듈은 한번만 로드된다. 이는 마치 require 함수로 가져온 모듈을 캐싱해 한번만 로드하는 CommonJS특징과 유사하지만 그방법은다르다.
-
import.meta 모듈의 정보를 제공하는 객체 ESModule명세에 포함된 특별한 내장객체로, 현재 모듈에 대한 정보를 포함하고 있다.
-
import.meta.url : 현재 모듈의 URL을 나타내는 문자열
-
import.meta.resolve(moduleName): 현재 모듈의 URL을 기반으로 모듈 지정자를 URL로 해석하는 메서드다.
- .mjs파일확장자 ECMAScript표준에서 ESModule을 도입하면서 브라우저나 Node.js같은 자바스크립트 런타임 환경에서 CommonJS와 구분하기 위해 새로운 파일 확장자가 추가됐다.
4.3.2.2 정적 모듈 로딩
- 빌드 시점에 모듈을 가져온다는 것을 의미. 즉 코드가 실행되기 전에 필요한 모듈이 이미 로드돼있으며, 이는 번들러가 소스코드를 번들링할때 모든 의존성을 파악하고 포함시키는 방식으로 이뤄진다. 따라서 빌드된 번들은 모듈을 동적으로 로드하는데 필요한 추가적인 네트워크 요청이나 로딩지연없이 필요한 모든 코드가 사전에포함돼있다.
이점
-
불필요한 대기 시간 감소
-
코드 예측가능
-
모듈 캐싱과 최적화
-
의존성 관리 용이
-
번들 최적화
- 필요에 따라 모듈을 동적으로 로드할수도있다. import() 함수를 통해 이뤄진다.
4.3.2.3 최상위 수준 await
모듈 전체가 하나의 거대한 비동기 함수로 동작할수있다. 이러한 특징을 최상위 수준 await이라고 한다.
4.3.2.4 ESModule의 동작 방식
모듈 파싱, 모듈 인스턴스화, 모듈 평가의 세단계
- 모듈 파싱 : 브라우저나 자바스크립트 엔진이 로드한 모듈 파일을 해석해 모듈 레코드를 생성하고, 해당 모듈의 구문과 의존성을 분석하는 과정이다. 이 단계에서 모듈 레코드가 생성됨으로써 모듈로더가 요청시 해당모듈을 추적하고 관리할수있다.
모듈 레코드는 실제 메모리에 로드된 모듈의 값을 관리하는 고조체다. 모듈레코드는 해당 모듈의 상태, export된 항목, 그리고 의존하는 모듈들의 정보를 포함하며, 구체적으로 어떤 필드로 이정보를 관리하는지 ESModule명세의 모듈레코드의 필드로 확인할수있다. 또한 이모듈레코드는 파싱단계에서 생성되지만 실제로 이레코드로 함수와 변수들이 메모리에 할당되고 모듈간의 참조 관계가 설정되는것은 모듈인스턴스화 과정에서 업데이트 된다.
파싱단계에서는 모듈레코드 생성뿐만아니라 생성하는 도중에 모듈이 문법적으로 유효한지를 확인하고 모듈의 구조를 이해하는 작업도 수반된다. 즉, 파싱과정은 모듈의 내용이 자바스크립트 코드로 해석될수있는지 확인하고 의존성을 분석하는 과정으로 이러한 피싱의 자세한 과정은 ESModule명세의 ParseScript, ParseModule에 나누어 기술돼있다.
- 문법검사
- 토큰화
- 구문분석
- 의존성분석
- 모듈인스턴스화
export된 값들이 메모리에 할당되고 초기화 되는단계 이과정에서 모듈은 비로소 사용가능한 상태가된다. import된 기능들도 메모리에 로드된다. 모듈레코드에 모듈정보를 업데이트해서 export와 import문이 해석되고 필요한 값들이 메모리에 할당되며 비로소 모듈간 참조가 연결된다 이와 같은 일련의 과정을 의존성 해결이라고 한다.
중요한점은 export와 import문이 동일한 메모리 주소를 참조한다는 점
- 모듈평가
해당모듈이 실제로 실행된다. 모듈내의 모든 코드가 평가되어 실행되며 export된값들이 최종적인 결과값을 갖는다.
- 동작방식정리
[모듈파싱] 시작 -> 로드한 모뮬의 구문을 분석 -> import문으로 다른 모듈의 의존성을 분석 -> [모듈인스턴스화] export 문을 따라 의존성의 가장 마지막 지점까지 모듈을 인스턴스화 -> import문을 따라 가져올 모듈을 연결하여 모듈 레코드를 완성 -> [모듈평가] 코드를 실행하여 실제 메모리에 모듈의 내용을 로드 -> 종료
4.3.2.5 ESModule의 순환참조
A모듈이 B모듈을 가져오고, B모듈이 A모듈을 가져오는 상황
ESModule에서는 순환참조 문제를 해결하기 위해 자바스크립트의 비동기 메커니즘인 Promise를 활용 순환참조가 발생하더라도 Promise를 사용해 무한루프에 빠지지 않고 안전하게 처리가능
4.3.3 Node.js의 ESModule
Node.js가 ESModule을 지원하게된 배경
-
표준화
-
성능 최적화
4.3.3.1 ESModule로더
Node.js에서 ESModule파일을 로드하는 로더의 특징
-
비동기적 로딩
-
몽키 패치불가 : 프로그램의 메모리 내 소스 내용을 런타임중 직접변경하는 방식 불가
-
폴더 모듈 사용 불가
-
확장자 명시 필요
-
CommonJS와의 상호운용성 : ESModule로더는 CommonJS모듈을 로드할수있다.
4.3.3.2 ESModule파일규칙
-
.mjs확장자로 끝나는 파일 , .mjs확장자를 가진 파일은 항상 ESModule로 해석된다..
-
가장가까운 상위 package.json의 type필드가 module인 하위 .js파일 .js확장자를 가진 파일이더라도 상위 디렉터리에 있는 package.json파일에 "type": "module"이 설정된경우 해당파일을 ESModule로 처리한다.
-
--eval이나 STDIN으로 실행시 --input-type=module 플래그사용
-
--experimental-detect-module 옵션을 사용한겨웅
4.3.3.3 import.meta
-
import.meta.url
-
import.meta.dirname 과 import.meta.filename
-
import.meta.resolve(specifier)
-
상대 및 절대경로 해결
-
모듈 황작자 고려
-
파일 및 디렉터리 확인
-
파일시스템 상의 실제경로해석
4.3.3.4 CommonJS와의 상호운용성
- import과 require() 함수
-
동기 vs 비동기 로딩
-
전역 객체 차이
-
환경설정
- CommonJS 네임스페이스
4.4 Node.js는 어떻게 node_modules에서 패키지를 찾아갈까?
4.4.1 모듈 해석 아고리즘
모듈을 로드하고 의존성을 해결하는 일련의 과정을 모듈해석알고리즘이라고한다.
알고리즘은 CommonJS와 ESModule마다 세부적인 차이는 있지만 기본적인 원칙과 흐름은 유사
4.4.1.1 모듈지정자
require()함수, import문에서 특정 모듈을 식별하는 문자열 모듈을 찾고 로드하는데 사용된다. 크게 3가지 유형
-
상대 경로 지정자
-
모듈 이름 지정자
-
절대 경로 지정자
모듈이름지정자는 Node.js의 모듈해석 알고리즘에 의해 특별히 처리됨 다른지정자는 표준적인 URL경로 해석 방식으로 해결