어플리케이션의 크기가 커져 파일을 분리했을 때 분리된 각각의 파일을 모듈이라고 부른다.
모듈은 대개 클래스 하나 또는 같은 목적을 가진 여러개의 함수로 구성된다.
스크립트의 크기가 점차 커지자 자바스크립트 커뮤니티는 다음과 같은 모듈을 만들었다.
모듈은 파일 하나에 불과하다.
export
, import
를 사용해서 다른 모듈을 불러올 수 있다.
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // 함수
sayHi('John'); // Hello, John!
브라우저에서 모듈은 <script type="module">
속성을 사용해서 스크립트가 모듈이라는 것을 브라우저가 알 수 있도록 해야한다.
모듈은 항상 엄격 모드로 실행된다.
모듈은 자신만의 스코프가 있다. 외부에서 사용하게 하려면 export
를 사용한 뒤 사용하려는 곳에서 import
를 해야한다.
동일한 모듈이 여러곳에서 사용되더라도 모듈은 최초 호출 시 단 한 번만 실행이 되고 실행 결과는 모듈을 가져가려는 모든 모듈에 export된다.
// 📁 alert.js
alert("모듈이 평가되었습니다!");
// 동일한 모듈을 여러 모듈에서 가져오기
// 📁 1.js
import `./alert.js`; // 얼럿창에 '모듈이 평가되었습니다!'가 출력됩니다.
// 📁 2.js
import `./alert.js`; // 아무 일도 발생하지 않습니다.
이러한 특징 때문에 모듈 설정을 쉽게 할 수 있다. 최초로 실행되는 모듈의 객체 프로퍼티를 원하는대로 설정하면 다른 모듈에서 이 설정을 그대로 사용할 수 있기 때문이다.
import.meta
객체는 현재 모듈에 대한 정보를 제공한다.
브라우저 환경에서는 스크립트의 url 정보를 얻을 수 있다.
모듈 최상위 레벨의 this는 undefined이다.
모듈이 아닌 일반 스크립트의 this는 전역객체인 것과 대조된다.
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
브라우저 환경에서 모듈 스크립트는 항상 지연 실행된다. 외부 스크립트, 인라인 스크립트와 관계 없이 마치 defer
속성을 붙인 것 처럼 실행된다.
defer, async 스크립트
브라우저는 HTML을 읽다가 <script>...</script>
태그를 만나면 스크립트를 먼저 실행해야 하기 때문에 DOM 생성을 멈춘다. 이런 동작 방식은 두 가지의 문제를 만든다.
이런 문제를 해결하기 위해서 script 태그를 페이지의 맨 마지막에 넣는 방법이 있다. 그런데 HTML 문서 자체가 매우 큰 경우는 페이지가 매우 느려질 가능성이 있다.
이런 상황에서 defer, async를 사용할 수 있다.
⭐ defer
defer는 스크립트를 백그라운드에서 다운로드 한다. 따라서 스크립트를 다운로드 하는 중에도 HTML 파싱을 멈추지 않는다. 그리고 스크립트는 페이지 구성이 완료되면 실행이 된다. 스크립트가 DOM에 의존하고 있다면 defer가 유용하다.
⭐ async
async는 defer와 마찬가지로 페이지와 독립적으로 동작한다. async 스크립트는 다운로드가 되기를 기다리지 않고 페이지 내 콘텐츠를 처리한다. 이후에 스크립트의 실행 중에는 HTML 파싱을 멈춘다.
async 스크립트들은 서로를 기다리지 않고 다운로드가 된다. 그리고 다운로드가 완료된 순서대로 실행이 된다. 따라서 다른 스크립트에 종속적이지 않은 스크립트를 실행할 때 유용하다.
브라우저 환경에서는 import는 반드시 url 앞에 와야한다.
import {sayHi} from 'sayHi'; // Error!
// './sayHi.js'와 같이 경로 정보를 지정해 주어야 합니다.
// 배열 내보내기
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// 상수 내보내기
export const MODULES_BECAME_STANDARD_YEAR = 2015;
// 클래스 내보내기
export class User {
constructor(name) {
this.name = name;
}
}
// 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export {sayHi, sayBye}; // 두 함수를 내보냄
// 📁 main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
하지만 이렇게 하는 것보다 가져올 것을 구체적으로 명시하는 것이 좋다.
// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';
hi('John'); // Hello, John!
bye('John'); // Bye, John!
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
export default를 사용하면 해당 모듈에 개체가 하나만 있다는 것을 명확히 나타낸다.
// 📁 user.js
export default class User { // export 옆에 'default'를 추가해보았습니다.
constructor(name) {
this.name = name;
}
}
export ... from ...
문법을 사용하면 가져온 개체를 즉시 ‘다시 내보내기(re-export)’ 할 수 있다.
npm 패키지를 만들었을 때 유용하게 사용할 수 있다.
import(module)
표현식은 모듈을 읽어서 모듈이 내보내는 것을 모두 포함하는 객체를 담은 이행된 프로미스를 반환한다.
let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");
import(modulePath)
.then(obj => <모듈 객체>)
.catch(err => <로딩 에러, e.g. 해당하는 모듈이 없는 경우>)