1. 모듈이 필요한 이유
소프트웨어가 커지면 발생하는 문제들
소프트웨어는 시간이 지나면 지날수록 눈덩이처럼 커지고 복잡해집니다.
초기에는 단순히 HTML, CSS, JavaScript 파일 한 두 개로 시작한 자그마한 웹사이트가, 수백 개 이상의 파일과 수십 개의 라이브러리가 함께 동작하는 거대한 시스템을 이루기도 합니다.
물론 몸집만 커지면 정말 좋겠지만, 이런저런 문제가 발생하는 건 필수 불가결한 일이죠.
- 전역 스코프 오염
여러개의 스크립트가 하나의 전역 객체에 변수나 함수를 추가하면 충돌이 발생할 수 있습니다.
<script src="utils1.js"></script>
<script src="utils2.js"></script>
<script>
console.log(square(4));
</script>
//utils1.js
function square(num) {
return num * num;
}
//utils2.js
function square(num) {
return num + num;
}
< 에이 누가 저걸 헷갈려서 동일한 이름을 써요 ]
라고 말할 수 있지만, 만약 서비스가 정말 커지고 파일의 수가 많아졌을 때 저런 일이 일어나지 않는다고 확신할 수 있나?라고 물어보면...
가끔 내가 뭐라 말하는지 모를 때도 있는데..
이것 이외에도
- 특정 파일이 어떤 파일에 의존하는지 명확하지 않으면 코드 유지보수가 어려운 만큼 의존성 관리의 어려움.
- 또는 동일 기능을 여러 파일에서 중복해서 작성해야 하는 재사용성 부족의 문제들이 발생할 수 있습니다.
사람이 마구잡이로 살이 찌면 덩치가 커짐과 동시에 이런저런 건강사항의 에러가 발생합니다.
그거랑 똑같습니다. 서비스도 그냥 살이 많이 찌면 여러 문제가 생깁니다.
그래서 건강하게 살을 찌울 수 있도록 바로 이 모듈화 라는 개념이 등장하게 됩니다.
모듈이란 무엇인가?
모듈(Module)의 사전적 의미는 프로그램을 구성하는 구성요소의 일부를 의미합니다.
쉽게 말해서, 개발하는 어플리케이션의 크기가 커지면 언젠간 파일을 여러 개로 분리해야 하는 시점이 오는데 이때 분리된 파일 각각을 모듈이라고 부릅니다.
보통 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 이루어집니다.
JavaScript의 모듈화 : 없어요
재밌는 점은, JavaScript에는 원래 모듈 시스템이 존재하지 않았다는 점입니다.
JS가 만들어진지 얼마 되지 않았을 때는 스크립트의 크기도 작고 기능도 단순해서 긴 세월동안 모듈 관련 표준 문법 없이 성장할 수 있었습니다. 딱히 새로운 문법을 만들 필요성을 느끼지 못한 것이죠 🤨
그런데 스크립트의 크기가 점차 커지고 기능도 복잡해지자 JS 커뮤니티는 슬슬 다양한 시도를 하기 시작합니다.
특별한 라이브러리를 만들어 필요한 모듈을 언제든 불러올 수 있게 한다거나, 코드를 모듈 단위로 구성하는 등의 방법을요.
그 시도들이 바로 아래 네 가지 모듈 시스템입니다.
- AMD
- CommonJS
- UMD
- ESM
전 사실 CommonJS, ESM를 제외한 나머지 시스템은 잘 알지 못했습니다..ㅎ-ㅎ 물론 저 두 가지도 잘 알고 있는 건 아니었지만요.
모듈 시스템은 2015년에 표준으로 등재되었습니다. 이 이후로 진화에 진화를 거듭하며 이제는 대부분의 주요 브라우저와 Node.js가 모듈 시스템을 지원하고 있죠.
2. JavaScript 모듈 시스템의 변화
원시 모듈 관리 방식 🦧
앞서 설명했듯이, JavaScript는 초창기에는 모듈 시스템을 고려하지 않고 생긴 언어였습니다.
그렇기에 브라우저에서 JavaScript 코드를 실행하는 가장 기본적인 방법은 <script> 태그를 이용하는 것이었습니다.
<script src="utils.js"></script>
<script src="app.js"></script>
이 방식은 작은 프로젝트에서는 문제 없이 잘 쓰였지만 코드가 많아지고 의존성이 하나씩 추가될수록 숨겨져 있던 문제가 드러났습니다.
대표적인 문제가 전역 네임스페이스의 오염과 로드 순서 문제가 있었습니다.
이를 해결하기 위해 개발자들은 네임 스페이스 패턴, 즉시 실행 함수 표현식(IIFE)을 이용했습니다.
var myModule = (function () {
var privateVar = "비공개";
function publicFunction() {
console.log("공개");
}
return {
publicFunction: publicFunction,
};
})();
myModule.publicFunction(); // 공개
이 방법으로 전역 스코프 오염은 막았지만, 모듈간 의존성의 정의가 어렵다는 문제가 있었습니다.
CommonJS
2009년에 Node.js가 등장하면서 JavaScript는 브라우저를 벗어나 서버로 한 발짝 발을 담그게 됩니다.
공식적으로는 브라우저만 지원했기에 이를 서버사이드 및 데스크탑 어플리케이션에서 지원하기 위해 바로 CommonJS가 등장합니다.
CommonJS의 가장 큰 특징은 바로 require 모듈 로딩 방식입니다.
다른 모듈을 사용할 때는 require를, 모듈을 해당 스코프의 밖으로 보낼때는 module.exports를 사용하는 방식으로 Node.js에서 현재 이 방식을 사용하고 있습니다.
// utils.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// app.js
const utils = require("./utils");
console.log(utils.add(2, 3)); // 5
위 두 파일이 같은 디렉토리 내에 있다고 가정했을 때, 위와 같이 사용할 수 있습니다.
- require()를 이용해 동기적으로 모듈을 불러옴
- module.exports를 사용하여 모듈을 외부에 공개
- 실행 시점에 require()가 평가되어 동적 로딩 가능
- 서버 환경에는 적합하지만 브라우저 환경에는 적절하지 않음
[ 동기 로딩이 성능 문제를 일으킴 ]
Node.js에서는 CommonJS가 강력한 녀석이지만 브라우저 환경에서는 브라우저가 모듈을 직접 로드해야 했기 때문에 CommonJS는 적합하지 않았습니다.
그렇기에 이를 해결하기 위해 그다음으로 AMD가 나오게 됩니다.
AMD (Asynchronous Module Definition)
CommonJS가 서버 환경에서 동작하는 방식이라면, AMD는 브라우저 환경에서 비동기적으로 모듈을 로드하는 방식이었습니다.
이 방식에서 사용하는 함수는 define()과 require()이 있으며, 가장 잘 구현한 모듈로더는 RequireJS가 있습니다.
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<script data-main="index.js" src="require.js"></script>
</body>
</html>
require.js 파일을 받아 <script> 태그에 넣어주고 data-main의 속성으로 require.js가 로드되자마자 index.js가 실행되게 됩니다.
// index.js
require.config({
baseUrl: '/',
paths: {
a: 'a',
b: 'b',
}
});
require(['a'], (a) => {
a.printA();
});
require.config에서 기본 경로와 모듈의 경로를 설정해 줍니다.
require를 통해 첫 번째 인자에 해당하는 모듈이 로드되면, 그걸 a로 받아 printA 함수를 호출하는 콜백함수를 실행합니다.
// a.js
define(() => {
return {
printA: () => console.log('a')
}
});
모듈 a는 위와 같이 define()을 통해 정의됩니다. 여기서도 물론 위의 require()의 의존성 모듈처럼 실행 전에 로드돼야 하는 모듈을 설정해 줄 수 있습니다.
- define()을 사용해 모듈을 정의 후 require()를 통해 비동기적으로 로드 가능
- 브라우저 환경에 최적화된 모듈 로더 방식
- 하지만 require()의 콜백 방식이 콜백 지옥을 만들 수 있음
AMD도 쓰이긴 했지만 인간의 욕심은 끝이 없고..
지금까지 나온 CommonJS와 AMD를 통합해 봐? 해서 나온 녀석이 그다음 UMD입니다.
UMD (Universal Module Definition)
모듈 구현 방식이 CommonJS와 AMD로 나뉘기에 그것들을 통합한 하나의 패턴이라고 할 수 있습니다.
공식 UMD 코드를 보시면 더 자세히 아실 수 있지만, 여기선 간단하게만 설명하고 넘어가겠습니다.
- AMD: define() 이 함수이고 define.amd 속성의 객체를 가지고 있다.
- CommonJS: module 이 객체이고 module.exports 속성의 객체를 가지고 있다.
- Browser: 따로 특이사항이 없다.
그리고 마지막으로, 가장 익숙한 ES6이 등장합니다.
ESM (ES6의 모듈시스템)
저도 그렇고, 많은 분들이 아마 익숙할 import와 export 구문을 사용하는 방식입니다.
2015년에 표준으로 도입되면서, JavaScript의 공식적인 모듈 시스템으로 현재 대부분의 최신 브라우저와 Node.js에서 지원됩니다.
// moduleA.js
const A = () => {};
export default A;
//moduleB.js
export const B = () => {};
//index.js
import A from 'moduleA';
import { B } from 'moduleB';
여기서 조금 주의 깊게 볼 점은 default의 유무입니다.
export를 사용할 때는 named export와 default export를 사용할 수 있습니다.
하지만 default export는 모듈 내에서 한 번만 사용 가능하고, named export는 여러 번 사용할 수 있습니다.
물론 조금 귀찮은 단점도 있습니다. default export는 import에서 이름 그대로 바로 사용하면 되지만, named export는 {}로 묶어서 불러와야 합니다.
물론 * 와일드카드를 통해 그냥 싹 다 불러오거나 alias를 as로 주어 원하는 이름으로 사용할 수 있습니다.
- import와 export를 사용해 정적 모듈 지원
- 비동기 로딩이 기본이기 때문에 브라우저 환경에서 최적화 가능
- 트리 셰이킹 최적화가 가능하여 사용되지 않는 코드 제거 가능
- <script type="module">을 사용해 브라우저에서 네이티브 모듈 직접 로드 가능
등의 장점을 꼽아볼 수 있겠습니다.
3. CommonJS vs ESM
서버 vs 브라우저에서 가장 많이 사용하고 가장 반대되는 부분이 많은 만큼 두 가지를 비교해 보며 마무리하도록 하겠습니다.
CommonJS
- 동기적으로 require()를 실행합니다.
- require()가 런타임에 실행되므로 실행 시점까지 모듈 구조를 알 수 없습니다.
- Webpack 같은 번들러가 코드 최적화를 하기 어렵습니다.
- require()를 호출하는 순간 해당 파일이 즉시 실행되고, 동기적 로딩이기 때문에 코드가 위에서 아래로 차례대로 실행됩니다.
- 런타임 중에도 require()를 호출해 동적으로 모듈을 가져올 수 있습니다.
ESM
- 비동기적으로 import를 실행합니다.
- 코드 실행 전에 import를 정적으로 분석할 수 있습니다.
- 브라우저에서는 <script type="module">을 사용하여 비동기적으로 모듈을 로드합니다.
- 동적으로 로드하려면 import()를 사용해야 합니다.
- 정적 분석 가능
- 트리 셰이킹 가능
- 트리 셰이킹이란 사용되지 않는 코드를 제거하여 번들 크기를 줄이는 최적화 기법입니다.
- 정적 분석이 가능하여 트리 셰이킹이 가능하고 → 불필요한 코드를 제거할 수 있습니다.
- 병렬 로딩 최적화 가능
- 모듈이 순환 참조(circular dependency)를 가질 경우 실행 순서가 중요해집니다.
CommonJS | ESM | |
내보내기 방식 | module.export = {} or export.함수명 = 함수 | export or export default |
불러오기 방식 | const 모듈 = require('파일경로') | import {함수} from '파일경로' |
로딩 | 동기적 (require 실행 시 바로 실행) | 비동기적 (정적 분석 가능) |
트리 셰이킹 | 𝙓 | ✔️ |
브라우저 지원 | 𝙓 | ✔️ (ES6 이상 브라우저) |
Node.js 지원 | ✔️ | ✔️ (Node.js 12+부터) |
출처
'Language > JavaScript' 카테고리의 다른 글
자바스크립트의 비동기와 콜백 (1) | 2025.04.27 |
---|---|
호이스팅(Hoisting) 이해하기 (0) | 2025.04.08 |
자바스크립트의 모듈 : 순환 참조 (0) | 2025.03.12 |
나는 DOM이 무엇인지 알고 사용하고 있었을까? (0) | 2025.02.02 |