자바스크립트 모듈 시스템의 역사
현재 메인으로 진행하고 있는 프로젝트에서 사용하고 있는 패키지매니저를 두고 이런 질문을 받았다,
왜 yarn을 사용하시나요? 다른 패키지 매니저와 차이점은 알고 있나요?
패키지매니저를 비교하면서 자바스크립트의 모듈이라는 개념에 대해 자세하게 다뤄보았다.
You Don't Know JS Yet(YDKJSY) 책이 도움이 되었다.
처음 자바스크립트를 배울 땐 import, export가 자연스럽게 느껴졌지만 이는 자바스크립트가 발전되어 온 역사의 결과물이었고..
수년간 발전을 거듭해온 결과 지금 이렇게 편리하게 사용할 수 있던 거였다!
자바스크립트(JS)라는 언어는 태생부터 모듈이라는 개념이 없었다.
원래 웹페이지에 작은 기능을 붙이는 정도로만 쓰였기 때문에, 여러 파일로 나눠서 개발한다는 개념 자체가 없었던 것이다.
전역 스코프의 혼돈
초창기 JS는 모든 스크립트가 하나의 전역 공간에서 돌아갔다. 서로 다른 파일의 변수나 함수가 충돌하는 일은 흔했고, 규모가 커질수록 유지보수는 점점 힘들어졌다.
이 시기에는 그저 <script> 태그를 여러 개 나열해서 쓰는 것이 일반적이었고, 스크립트 로딩 순서 문제도 골칫거리였다.
<script>
var message = 'Hello from file1';
function greet() {
console.log(message);
}
</script>
<script>
var message = 'Hello from file2'; // 전역 변수 덮어씌워짐
greet(); // Hello from file2
</script>
CommonJS (CJS) - 서버의 요구로 등장한 모듈 시스템
JS가 서버로 확장되면서 변화가 시작되었다.
Node.js는 JS를 서버에서도 사용할 수 있게 해 줬지만, 전역 스코프 방식으로는 복잡한 서버 로직을 감당할 수 없었다.
그래서 CommonJS, 줄여서 CJS가 등장한다. CJS는 간단한 문법으로 모듈을 정의하고 불러올 수 있도록 했다.
이 방식은 동기적이기 때문에, require()를 호출하는 순간 해당 파일을 읽고 바로 실행한다.
Node 환경에서는 파일 시스템 접근 속도가 빠르기 때문에 이런 동기 방식이 잘 맞았다.
// math.js
module.exports = {
add: (a, b) => a + b
};
// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
Asynchronous Module Definition (AMD) - 브라우저에서 비동기 로딩을 해결
서버와 브라우저의 상황은 달랐다.
파일을 서버에서 받아와야 하는데, 동기적으로 모듈을 로드하면 화면이 멈추는 문제가 생겼다. 이를 해결하기 위해 나온 것이 AMD다.
AMD는 모듈을 비동기적으로 불러오고, 콜백 안에서 사용할 수 있게 했다.
AMD는 의존하는 모듈들을 define 함수의 인자로 선언한 뒤, 모든 모듈이 로드되면 콜백 함수 안에서 로직을 실행하게 되어 있다.
문제는 의존 모듈이 많아지고, 그 안에서 또 콜백이 중첩될 때였다. (콜백 지옥)
중첩 깊이가 깊어질수록 코드 읽기가 어렵고, 유지보수도 힘들어진다.
// main.js
require(['moduleA'], function (moduleA) {
moduleA.getUser(function (user) {
require(['moduleB'], function (moduleB) {
moduleB.getPosts(user.id, function (posts) {
require(['moduleC'], function (moduleC) {
moduleC.showPosts(posts);
});
});
});
});
});
Universal Module Definition (UMD) - 라이브러리 배포를 위한 만능 포맷
CJS와 AMD는 서로 호환되지 않았다. 그래서 어떤 라이브러리를 배포할 때, 사용자 환경이 CJS인지 AMD인지 고려해야 했다.
이 문제를 해결하기 위해 등장한 것이 UMD다.
UMD는 환경을 자동 감지해 CJS, AMD, 브라우저 전역 변수 중 적절한 방식을 적용한다.
이 방식은 Lodash, jQuery, Moment.js 와 같은 라이브러리에 사용되었고, 여전히 일부 라이브러리에서 볼 수 있다.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.MyLib = factory();
}
}(this, function () {
return { hello: () => console.log('Hello!') };
}));
ES Module (ESM) - JS 표준 모듈 시스템
오늘날 가장 많이 사용하는 ESM. 2015년 ECMAScript6 (ES6)에서 등장한 공식 모듈 시스템이다.
ESM은 다음 기능들을 제공한다.
- 정적 분석: 코드를 실행하지 않고, 코드만 읽어서(import/export) 모듈의 구조, 의존성 등을 미리 분석
- 트리 쉐이킹: 정적 분석을 기반으로, 실제로 사용되지 않는 코드를 번들에서 제거하는 최적화 기술
- 스코프 보호: 모듈마다 독립된 스코프를 가지기 때문에 전역 오염 없이 안전한 코드 작성 가능 (충돌 방지)
브라우저도 <script type="module">을 지원하고, Node.js도 ESM을 공식 지원하면서 현대 JS의 기본 모듈 시스템이 되었다.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3));
마무리하며
JS의 모듈 시스템은 단순히 문법의 변화로 발전해온 것이 아니라, 환경의 요구에 따라 적응해온 역사였다.
전역 변수 충돌, 스크립트 로딩 순서 문제, 콜백 지옥, 복잡한 번들링 설정... 등의 문제들
서버는 동기 방식을, 브라우저는 비동기 방식을 필요로 했고, 서로 다른 환경을 만족시키기 위한 시도들
이 모든 게 합쳐져 모듈 시스템 발전의 배경이 되었다.
지금 우리가 당연하게 쓰는 import/export 문법은 그 결과물이었던 것.
더 좋은 구조, 더 안전한 설계를 만들 수 있게 되어 다행이고 감사하다.
'기술 스택 > JavaScript' 카테고리의 다른 글
[패키지 매니저] pnpm, yarn, npm 비교와 사용 경험 (0) | 2025.04.04 |
---|---|
브라우저의 렌더링 과정 (0) | 2025.03.19 |
SPA란 (1) | 2024.01.04 |
[nodeJS] 백엔드와 브라우저 간 상호작용 (세션, 브라우저, 쿠키) (0) | 2023.03.31 |