애플리케이션의 크기가 커질 때 이를 분리한 재사용 가능한 코드 조각을 모듈이라고 합니다. 모듈은 하나의 파일이며 자신만의 파일 스코프를 가지게 됩니다.
자바 스크립트의 모듈은 단순한 보조기능 처리를 위해 제한적 용도로 태어났습니다. 때문에 클라이언트 사이드(브라우저)의 자바스크립트는 파일마다 독립적 파일스코프를 갖지 않습니다. 자바스크립트 파일을 여러개의 파일로 분리해 script
태그로 로드해도 하나의 파일 내에 있는 것처럼 동작합니다. 그래서 하나의 전역을 공유하게 됩니다.
스크립트의 크기가 점점 커지고 복잡해지면서 문제가 많았고, 브라우저 환경에 국한하지 않게 범용적 사용을 위해 CommonJS와 AMD가 등장했습니다.
ES6에서 모듈 기능을 추가하여 ESM을 사용할 수 있게 되었습니다. 일반 자바스크립트 파일이 아닌 ESM을 확실히 하기 위해 확장자로 .mjs
를 사용하는 것을 권장합니다. 브라우저 환경에서는 .js
확장자를 사용해도 type="module"
만 있으면 ESM으로 취급되어 차이점이 없습니다. 하지만 Node.js환경에서 .mjs
는 자동으로 ESM파일임이 명시되지만 .js
파일은 package.json
에 명시된 모듈 유형에 따라 달라집니다.
<script type="module" src="app.mjs"></script>
하지만 브라우저들은 파일 확장자에 상관 없이 type="module"
과 함께 쓰이면 모듈로 취급합니다.
<script type="module" src="app.js"></script>
클래스처럼 기본적으로 strict mode가 적용됩니다.
ESM은 독자적 모듈 스코프를 갖습니다. ESM이 아닌 자바스크립트 파일은 script 태그로 분리해서 로드해도 독자적인 모듈 스코프를 갖지 않습니다. 아래의 예제 코드를 실행시켜보면 전역변수 x
가 덮어씌워져 출력되는 것을 확인할 수 있습니다.
//📁 foo.js
var x = 'foo';
console.log(window.x);
//📁 bar.js
var x = 'bar';
console.log(window.x);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="foo.js"></script>
<script src="bar.js"></script>
</body>
</html>
ESM을 사용하면 var키워드로 선언한 변수는 전역 변수가 아니며 window객체의 프로퍼티도 아닙니다.
//📁 foo.js
var x = 'foo';
console.log(x);
console.log(window.x);
//📁 bar.js
var x = 'bar';
console.log(x);
console.log(window.x);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="foo.js"></script>
<script type="module" src="bar.js"></script>
</body>
</html>
위에서 모듈은 독자적인 스코프를 갖는다고 말했습니다. 그렇기에 내부에서는 자유롭게 사용할 수 있습니다. 외부에서 다른 모듈들이 재사용할 수 있게 하려면 export 키워드를 넣어줘야 합니다.
//📁 pizz.js
export const 고기 = "소고기";
export const 과일 = "파인애플";
export const 소스 = "바질페스토";
export function showToppings() {
console.log(`토핑 종류 : ${고기}, ${과일}, ${소스}`);
}
export const Pizza = (function () {
function Pizza(topping) {
this.topping = topping;
}
Pizza.prototype.bake = function () {
console.log(`${this.topping} 피자를 구웠습니다! 맛있게 드세요!`);
}
return Pizza;
})()
아래와 같이 한 번에 객체로 export도 가능합니다.
const 고기 = "소고기";
const 과일 = "파인애플";
const 소스 = "바질페스토";
function showToppings() {
console.log(`토핑 종류 : ${고기}, ${과일}, ${소스}`);
}
const Pizza = (function () {
function Pizza(topping) {
this.topping = topping;
}
Pizza.prototype.bake = function () {
console.log(`${this.topping} 피자를 구웠습니다! 맛있게 드세요!`);
}
return Pizza;
})()
export {고기, 과일, 소스, showToppings, Pizza} //객체로 한번에!
//export {고기 as meat, 과일 as fruit, 소스 as sauce, showToppings, Pizza}
//as를 통해 이름을 바꿔 내보낼 수도 있다
또는 export default
를 사용할 수도 있습니다. 이 경우엔 모듈 이름과 상관 없이 as
를 쓰지 않고 사용이 가능하게 됩니다. 해당 모듈 안에는 개체가 하나만 있기 때문입니다.
// 📁 user.js
export default class User { //import User from './user.js'
constructor(name) {
this.name = name;
}
}
//export {User as default};
다른 모듈에서 export한 식별자를 자신의 모듈 스코프 내부로 로드하려면 import 키워드를 사용하면 됩니다. 다른 모듈에서 export한 식별자 이름으로 import할 수 있습니다.
//📁 app.js
import { 고기, 과일, 소스, showToppings, Pizza } from './pizza.js';
console.log(과일);
showToppings();
const pineapplePizza = new Pizza(과일);
pineapplePizza.bake();
하지만 import할 식별자가 너무 많다면 위와같이 지저분하기도 하고 불편합니다. 이럴 땐 as를 사용해 하나의 이름으로 한 번에 import할 수 있습니다.
//📁 app.js
import * as pizza from './pizza.js';
console.log(pizza.과일);
pizza.showToppings();
const pineapplePizza = new pizza.Pizza(pizza.과일);
pineapplePizza.bake();
브라우저 환경에서 모듈 스크립트는 항상 지연 실행됩니다. 모듈 스크립트는 HTML 문서가 준비될 때 까지 대기하다가 HTML 파싱이 끝난 이후에 실행됩니다. 또한 스크립트의 상대적 순서가 유지됩니다.
<!--📁 index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<h3></h3>
</div>
</body>
<script type="module" src="./defer.js"></script>
<script src="./script.js"></script>
</html>
//📁 script.js
const h = document.querySelector('h3');
alert(`일반 스크립트 실행`);
//중단점을 찍어 보면 h는 null로 아래 코드는 오류가 납니다.
h.innerHTML = "일반 스크립트에 의해 변경됨";
//📁 defer.js
const h = document.querySelector('h3');
alert(`모듈 스크립트 실행`);
//정상적으로 적용되는 걸 확인하실 수 있습니다.
h.innerHTML = "모듈 스크립트에 의해 변경됨";
만약 다른 스크립트나 html문서를 기다리지 않고 실행되도록 하려면 인라인 async
를 사용할 수 있습니다.
<script async type="module" src="./defer.js"></script>
동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출시 단 한번만 실행됩니다. 최초 초기화 작업에 사용됩니다.
//📁 pizzaCounterModule.js
let orderCount = 0;
export function orderPizza() {
orderCount++;
console.log(`피자를 주문했습니다! 현재까지 ${orderCount}번 주문되었습니다.`);
}
export function getOrderCount() {
return orderCount;
}
//📁 customerA.js
import { orderPizza, getOrderCount } from './pizzaCounterModule.js';
console.log("customerA에서 피자 주문합니다.");
orderPizza();
console.log(`customerA가 확인한 주문 횟수: ${getOrderCount()}번`);
//📁 customerB.js
import { orderPizza, getOrderCount } from './pizzaCounterModule.js';
console.log("customerB에서 피자 주문합니다.");
orderPizza();
console.log(`customerB가 확인한 주문 횟수: ${getOrderCount()}번`);
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>피자 모듈 예제</title>
</head>
<body>
<script type="module" src="customerA.js"></script>
<script type="module" src="customerB.js"></script>
</body>
</html>
type="module"
을 지원하지 않는 구식 브라우저에 대비하기 위해 nomodule
속성이 있습니다.
<script async type="module" src="./defer.js"></script>
<script nomodule src="./nomodule.js"></script>
https://ko.javascript.info/modules-intro
모던 자바 스크립트(위키북스)