모듈 시스템의 역사 되돌아보기

aken·2025년 3월 7일
0
post-thumbnail

모듈

모듈은 코드가 담긴 하나의 파일을 의미합니다.
어플리케이션이 커져서 하나의 파일을 여러 개로 분리하신 적 있지 않나요?
이렇게 분리된 파일들 각각을 모듈이라고 부릅니다.

모듈 시스템이 없던 시절에...

모듈 시스템이 없던 시절에는 전역 변수로 코드를 관리했습니다.
여러 JavaScript 파일들을 script 태그로 로드하더라도, 모듈 간의 스코프가 구분되지 않았습니다.
그로 인해, 개발자는 전역 변수가 오염되는 경우를 고려하여 다른 모듈에 변수 이름을 겹치지 않도록 조심해야 했습니다.

<body>
	<script>
		var message = "script1.js";
	</script>
	<script>
		var message = "script2.js";
	</script>
	<script>
        console.log(message); // script2.js 출력
    </script>
</body>

그리고 파일 간의 실행 순서를 수동으로 조정해야 했습니다.

<body>
	<script>
        sum(1, 2) // Uncaught ReferenceError: sum is not defined 
	</script>
	<script>
        function sum(a,b) { return a+b; }
	</script>
</body>

모듈 시스템이 없던 시절에는 script 태그를 위에서 아래로 순차적으로 실행했습니다.
첫 번째 script를 실행할 시점에는 sum 함수가 아직 선언되지 않아 참조 에러가 발생했습니다. 따라서 sum이 선언된 script를 먼저 실행해야 합니다.

<body>
  	<script>
        function sum(a,b) { return a+b; }
	</script>
	<script>
        sum(1, 2) // 3
	</script>
</body>

전역 변수의 오버라이딩 문제를 해결하기 위해 IIFE나 namespace pattern으로 전역 변수를 줄이려고 했습니다. 그러나 이 방법들도 script 실행 순서를 수동으로 관리해야하며, 전역 변수의 오버라이딩 문제를 완전히 방지할 수 없었습니다.

모듈 시스템

CommonJS

서버 사이드에서 JS를 실행하고 싶다!

등장 배경

초창기 JavaScript는 원래 브라우저에서만 실행되는 언어였습니다. 시간이 지날수록 점차 동적인 웹 어플리케이션으로 발전하면서 JavaScript의 역할이 커지기 시작했습니다.

특히, 2005년에 AJAX가 등장하게 되면서 JS로 HTTP 요청하고, UI를 조작할 수 있게 됩니다. 전보다 JS 연산이 많아지게 되었고 이 연산들을 빠르게 처리할 JS 엔진이 필요했습니다.

이런 배경에서, 2008년 구글에서 기존 JS 엔진보다 훨씬 성능이 좋은 V8 JS 엔진을 발표했습니다.
원래도 브라우저 외부에서 JS를 실행했으면 하는 요구가 지속적으로 있었는데, V8 엔진의 등장으로 서버사이드 JS에 대한 활동이 활발해졌습니다.

2009년에 Mozilla 엔지니어인 Kevin Dangoor가 본인 블로그JS가 서버 사이드에서 실행되려면 표준이 필요하다고 주장하면서 JS의 아쉬운 점도 나열했는데요. 그 중 하나는 module 표준이 없다는 점이었습니다.

JavaScript needs a standard way to include other modules and for those modules to live in discreet namespaces. There are easy ways to do namespaces, but there’s no standard programmatic way to load a module (once!). This is really important, because server side apps can include a lot of code and will likely mix and match parts that meet those standard interfaces.

Kevin은 서버 사이드 또는 데스크탑 어플리케이션과 같이 브라우저 외부에서 JavaScript를 사용하기 위해 CommonJS 그룹을 모집했고 모듈 시스템을 표준화하는데 성공합니다.

CommonJS is a project to standardize the module ecosystem for JavaScript outside of web browsers - wiki

사용 방법은 아래와 같습니다.

// sum.js 모듈 정의
module.exports = {
  add: (a, b) => a+b,
};
// 또는
exports.add = (a, b) => a+b;

// 모듈 사용
const sum = require('./sum.js')
  • 모듈 내보내기: module.exports 또는 exports
  • 모듈 사용: require

한계

CommonJS는 모듈을 동기적으로 로드합니다. 즉, require로 불러오는 모듈이 완전히 로드되고 실행될 때까지 다음 줄의 코드가 실행되지 않습니다.

이러한 동기적 로딩 방식은 주로 서버 환경에서 유리합니다. 서버는 모든 모듈이 로드된 후에야 코드를 실행하기 때문입니다.
하지만, 브라우저는 네트워크를 통해 모듈들을 비동기적으로 다운로드 받아야 사용할 수 있기 때문에 비동기 로드가 필수입니다.
앞에서도 말했지만, 애초에 CommonJS는 서버 사이드에서 JS를 사용하기 위해 만들어졌기에 브라우저보다 서버 사이드에서 사용할 때 더 장점이 많습니다.

AMD (Asynchronous Module Definition)

JS를 비동기적으로 로드하고 싶어!

등장 배경

CommonJS 그룹이 있듯이 AMD 그룹이 있습니다. 이 그룹은 JS 모듈을 비동기적으로 로드하기 위해 CommonJS에서 논의했으나 합의하지 못하고 독립한 그룹입니다.
즉, 이 그룹의 목표는 JS 모듈을 비동기적으로 로드하여 브라우저 환경에서도 모듈을 사용할 수 있도록 하는 것이었습니다.

// 모듈 정의

// js/add.js
define([], function() {
  // 외부에 노출할 함수를 반환
  return {
    add: function(a, b) {
      return a + b;
    },
  };
});

// js/hello.js
define([], function() {
  return {
    sayHello: function(name) {
      return "Hello, " + name + "!";
    }
  };
});

// 모듈 불러오기
// js/main.js
// 필요한 모듈들의 경로를 배열로 받기
require(['math', 'message'], function(math, message) {
  // 모듈들이 로드된 후 콜백 함수 실행
  var sum = math.add(5, 10);
  var greeting = message.sayHello("Alice");

  console.log("Sum: " + sum); 
  console.log("Greeting: " + greeting);
});

한계

코드를 딱 보면 문법이 복잡하고 가독성이 떨어집니다.

UMD (Universal Module Definition)

CommonJS와 AMD를 모두 지원하자

등장 배경

CommonJS는 서버 사이드에서, AMD는 브라우저에서 주로 사용되었습니다.
만약 모듈을 브라우저와 서버 사이드에서 동작하게 하려면 어떻게 해야할까요? 이 경우 UMD를 사용하면 됩니다.
UMD는 브라우저, 서버 사이드 등 어디에서나 작동할 수 있는 모듈 시스템을 만드는 것을 목적으로 CommonJS와 AMD 방식을 모두 지원합니다.

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD 환경
    define([], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS 환경
    module.exports = factory();
  } else {
    // 전역 객체에 모듈 할당
    root.myModule = factory();
  }
}(this, function () {
  // 모듈 정의
  var myModule = {};

  myModule.sayHello = function() {
    return "Hello, world!";
  };

  // 모듈이 제공할 기능을 반환
  return myModule;
}));

코드가 복잡하지만, CommonJS와 AMD를 통합하여 브라우저와 서버 사이드 등 다양한 환경에서 모듈을 사용할 수 있게 되었습니다.

ES Modules (ECMAScript Module)

JavaScript 자체에서 표준 모듈 시스템 등장

드디어 ECMAScript 2015에서 JavaScript 자체 표준 모듈 시스템 ESM이 등장합니다.

// hello.js
export function hello() {
  console.log(`Hello!`);
}

// main.js
import {hello} from './hello.js';

hello();

외부에서 사용할 수 있도록 변수나 함수 앞에 export를 작성하고, 사용할 땐 import로 가져옵니다.
그리고 브라우저에게 이 JS 파일이 모듈이라는 걸 알리기 위해 <script type="module"> 속성을 설정해야 합니다.

<script type="module" src="main.js"></script>

드디어 모듈은 자신만의 스코프를 가지게 됩니다. 외부 스크립트에서 모듈 내부에 정의한 변수에 접근할 수 없습니다.

<script type="module">
  let a = 1
</script>
<script type="module">
  console.log(a) // Uncaught ReferenceError: a is not defined 
</script>

브라우저 뿐만 아니라, Node.js에서도 ES Modules를 지원합니다. 프로젝트 전반적으로 적용하고 싶다면 package.json에 type 속성 값을 module로 설정하면 됩니다.

{
  "type": "module",
  ...
}

참고

JavaScript 표준을 위한 움직임: CommonJS와 AMD
UMD github
ES modules: A cartoon deep-dive
MDN JavaScript Modules
모듈 시스템의 역사, 그리고 ESM
JavaScript 번들러로 본 조선시대 붕당의 이해

0개의 댓글