애플리케이션의 규모가 점점 커지면서 자바스크립트 코드를 여러 개의 파일로 분리하여 작성하고 이를 필요한 시점에 불러와서 사용하며 효율적으로 관리할 수 있는 시스템이 필요해졌습니다.
이때 모듈(module) 이라는 단어가 등장합니다.
모듈은 자바스크립트 코드가 모여있는 하나의 파일입니다. 특정 기능이나 목적에 따라 분리되어 있는 코드가 모여있습니다.
이런 모듈이 여러 개 모여 프로그램을 구성할 수 있고, 해당 프로그램의 부품 역할을 합니다.
여러 개의 모듈이 생기면 각 모듈을 재사용하고, 쉽게 불러와서 사용할 수 있도록 관리하는 시스템이 필요합니다.
따라서, 모듈 시스템(module system) 이란 코드를 여러 파일로 분리하고 재사용성을 높이기 위한 기능을 제공하는 프로그램을 의미합니다.
이렇게 모듈화 했을 때 장점은 무엇이 있을까요?
최초의 자바스크립트는 다음과 같이 HTML에 script를 순서대로 로드하는 방식 의 아주 간단한 모듈 시스템을 제공했습니다.
<html>
<script src="/src/foo.js"></script>
<script src="/src/bar.js"></script>
<script src="/src/baz.js"></script>
</html>
여기에는 문제점이 있습니다.
변수의 이름이 중복된다면, 먼저 호출된 스크립트 내의 변수가 나중에 호출된 스크립트 내의 변수로 재정의 되면서 문제가 발생할 수 있습니다.
이는 모듈 간 스코프가 구분되지 않아 다른 파일을 오염시키는 문제가 발생했다는 의미입니다.
그래서 다른 모듈과 변수 이름이 겹치지 않도록 해야하고, 모듈의 로드 순서를 지정하는데에도 많은 시간을 쏟아야 했습니다.
이러한 문제는 CommonJS, AMD, UMD, ESM과 같은 자바스크립트 모듈 시스템의 등장으로 여러 방법을 통해 해결할 수 있게 되었습니다.
자바스크립트는 공식적으로 브라우저만 지원했습니다. 이러한 자바스크립트를 브라우저뿐만 아니라 서버사이드 애플리케이션이나 데스크탑 애플리케이션을 지원할 수 있도록 하기 위해 CommonJS가 등장하게 됩니다.
CommonJS는 자바스크립트를 범용적인 언어로 사용하기 위한 스펙을 정의하는데, 주요 명세는 모듈을 어떻게 정의하고 어떻게 사용할 것인가에 대한 것입니다.
CommonJS의 모듈화는 다음과 같이 세 부분으로 이루어집니다.
exports
객체를 이용한다.require
함수를 이용한다.현재 모듈에서 다른 모듈을 사용할 때 require
, 현재 모듈에서 다른 모듈로 내보낼 때는 module.exports
또는 exports
를 사용합니다.
// a.js
const printHelloWorld = () => {
console.log('Hello Wolrd');
};
// module.exports를 이용하여 모듈 내보내기
module.exports = {
printHelloWorld
};
// b.js
// require를 이용하여 a.js 가져오기
const func = require('./a.js'); // 같은 디렉토리에 있다고 가정
func.printHelloWorld();
exports
vs module.exports
모듈을 내보낼 때 module.exports
또는 exports
를 사용할 수 있습니다.
두 가지 방법의 차이점은 무엇일까요?
module.exports
// module.js
const person = {
name: "Bori",
greeting: "Hi"
}
module.exports = person;
// main.js
const user = require('./module');
console.log(user);
// 출력 결과
{ name: "Bori", greeting: "Hi" }
exports
// module.js
const person = {
name: "Bori",
greeting: "Hi"
}
exports = person;
// main.js
const user = require('./module');
console.log(user);
// 출력 결과
{ }
module.exports
,exports
의 차이만 있을 뿐 같은 방법으로 모듈 내보내기를 적용해보았습니다.
module.exports
는 person
객체를 정상적으로 가져왔지만 exports
는 빈 객체를 가져왔습니다.
exports
를 이용해 정상적으로 객체를 가져오기 위해서는 다음과 같이 작성해야 합니다.
// module.js
const person = {
name: "Bori",
greeting: "Hi"
}
exports.person = person; // 내보내기 할 때
// main.js
const user = require('./module');
console.log(user.person); // 가져온 모듈의 객체에 접근할 때
// 출력 결과
{ name: "Bori", greeting: "Hi" }
module.exports
와 exports
의 차이를 코드로 설명하자면 다음과 같습니다.
const module = {
exports: { }
};
const exports = module.exports;
function require(path) {
...
return module.exports;
}
기본적인 문법은 module.exports = expression
입니다.
module.exports
와 exports
는 동일한 객체를 바라보고 있지만, exports
는 module.exports
를 참조하는 형태 입니다.
따라서 exports = a
의 형태로 코드를 작성하면 module.exports
에 대한 참조가 끊어지고 a
의 값을 가지게 됩니다.
그래서 exports
는 프로퍼티에 접근하는 방식을 사용하고, module.exports
는 바로 사용할 수 있습니다.
또한 require
는 module.exports
를 반환 합니다.
만약 exports
에 어떤 값을 할당하거나 새로운 객체를 할당하더라도, require
는 module.exports
를 반환하기 때문에 잠재적인 버그를 피할 수 있습니다.
CommonJS는 트리쉐이킹(tree shaking)이 되지 않습니다.
트리쉐이킹이란, 임포트 되었지만 실제로는 사용되지 않는 코드를 제거하여 코드를 최적화 하는 기술을 의미합니다. 이로인해 번들의 크기가 문제로 이어지기도 합니다.
또한, 동기적으로 모듈을 호출하는 방식을 사용합니다. 따라서 각 모듈은 순서대로 하나씩 로드되고 실행되며 각 모듈이 필요하지 않은 시점에도 미리 로딩 해야합니다.
AMD 그룹은 비동기적으로 자바스크립트 모듈을 사용하기 위해 CommonJS에서 함께 논의하다가 합의점을 찾지 못하고 독립한 그룹 입니다.
AMD는 모듈과 종속성 파일들을 비동기적으로 로드할 수 있도록 모듈을 정의하는 매커니즘이라고 설명하고 있습니다.
브라우저에서는 모든 모듈이 다 로딩될 때가지 기다릴 수 없기 때문에 비동기적으로 모듈을 로딩하는 방식으로 구현되었습니다.
필요한 파일이 모두 로컬 디스크에 있어 바로 불러 쓸 수 있는 상황인 서버사이드에서는 CommonJS가 장점이 많은 반면, 필요한 파일을 네트워크를 통해 내려받아야 하는 브라우저 환경에서는 AMD가 장점이 더 많습니다.
CommonJS에서 분리되어 나온 그룹이기 때문에, require
와 exports
를 그대로 사용할 수 있다는 공통점도 가지고 있습니다.
AMD를 가장 잘 구현한 자바스크립트 로더에는 RequireJS가 있습니다.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>My Sample Project</title>
<!-- data-main 에는 require.js가 로드된 후 실행할 자바스크립트 파일 경로를 넣어준다. -->
<script data-main="js/app.js" src="scripts/require.js"></script>
</head>
<body>
<h1>My Sample Project</h1>
</body>
</html>
// module.js
define(function() {
return {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
}
};
});
// app.js
require(['module'], function (module) {
const result = module.add(1, 2);
console.log(result); // 3
});
require
require
의 첫 번째 인자는 불러올 모듈, 두 번째 인자는 모듈을 그대로 받아 해당 모듈 내의 함수를 호출하는 콜백함수를 실행합니다. 이를 통해 의존성 모듈을 지정합니다.
define
AMD의 특징 중 하나는 define
함수 입니다.
브라우저 환경의 자바사크립트는 파일 스코프가 따로 존재하지 않습니다. 따라서, define
이 파일 스코프 역할을 합니다.
일동의 네임스페이스 역할을 하여 모듈에서 사용하는 변수와 전역변수를 분리할 수 있습니다.
위의 예제에서 module은 define
를 통해 정의합니다. 또한 require
에서 의존성 모듈을 지정한 것처럼 콜백함수가 실행되기 전에 로드되어야 할 모듈을 지정할 수 있습니다.
CommonJS에 비해 사용 방법이 복잡해 보이지만 브라우저와 서버사이드에서 모두 호환되는 방식입니다.
참고
잘 읽었습니다. 좋은 정보 감사드립니다.