TL;DR


자바스크립트 코드의 모듈화는 코드를 효율적이고 조직적으로 구성할 수 있게 합니다. 쓰임새별로 잘 정돈된 서랍장처럼 코드를 쓰임새 별로 정리하면 코드를 읽을 때도 논리적이고 재사용하기도 용이합니다. 코드의 모듈화라고 하면 소스코드를 모듈 단위로 나누는 것을 의미하는데요, 자바스크립트 모듈은 기능 별로 코드를 나눈 단위를 말합니다.

본 글에서는 모듈이 갖는 특징과 사용법에 대해 알아봅니다.


모듈

코드 에디터에서 작성한 자바스크립트 스크립트 파일 하나가 모듈 하나입니다. 개발자는 export과 import 문을 사용해서 모듈 간 코드를 공유하죠. 우리는 어쩌면 이미 모듈 시스템으로 개발을 하고 있고 소스코드의 뼈대를 모듈로 구축하고 있죠.

export과 import 문과 관련해서는 이 글 참고.


자바스크립트 모듈

1.1 모듈


브라우저 환경에서 모듈을 사용하기 위해서는 script 태그에 type="module" 속성을 추가해 사용될 스크립트가 모듈이라는 걸 알려줍니다.

index.html
1<html lang="en">
2  <head>
3    <meta charset="UTF-8" />
4    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5    <script type="module" src="./main.js"></script> // 모듈 추가
6    <title>moonkorea</title>
7  </head>
8  <body>
9    <h1>moonkorea</h1>
10  </body>
11</html>
main.js
1import { categories } from './blog.js';
2console.log(categories.length);
blog.js
1export const categories = ['자바스크립트', '리액트'];

모듈은 http 또는 https 프로토콜을 통해서만 실행되고 file:// 프로토콜을 사용하는 로컬 파일에서는 동작하지 않습니다.


지연 실행

브라우저 환경에서 type="module" 속성이 추가된 스크립트와 일반 스크립트는 다르게 동작합니다.

모듈은 외부 스크립트, 인라인 스크립트와 관계없이 일반 스크립트에 defer 속성을 추가한 것처럼 동작합니다.

외부 모듈 스크립트

  • src 값이 동일한 외부 모듈 스크립트는 한 번만 실행됩니다.
html
1<script type="module" src="module.js"></script>
2
3<script type="module" src="module.js"></script>
  • 다른 오리진에서 모듈을 불러올 때는 CORS 헤더가 필요합니다.
html
1<script type="module" src="http://foo.com/module.js"></script>

따라서 브라우저는 외부 모듈 스크립트를 병렬적으로 불러오고 HTML 처리를 멈추지 않고 HTML 문서가 만들어진 후 실행됩니다. 스크립트의 실행 순서 또한 정의된 순서대로 차례로 실행됩니다.

index.html
1<script type="module">
2  alert(typeof button); // object
3</script>
4
5<script>
6  alert(typeof button); // undefined
7</script>
8
9<button id="button">클릭</button>

위 HTML에서는 페이지가 출력되기 전에 undefined가 출력되고 페이지가 모두 그려진 후에 object가 출력됩니다.

따라서 페이지 내에 특정 기능이 모듈에 의존적일 경우 모듈이 불러오기 전까지 UI를 로더(loader)나 투명 오버레이 등으로 처리하는 것이 사용자 경험에 좋습니다.

앞서 모듈은 외부 스크립트, 인라인 스크립트와 관계없이 defer 속성을 추가한 것처럼 동작한다고 했는데요, async 속성으로 비동기 로드할 경우 인라인 스크립트는 제대로 동작하지 않지만 모듈 스크립트에서는 async 속성을 인라인 스크립트에도 적용할 수 있습니다.

index.html
1// 외부 스크립트, ok
2<script async src="module.js"></script>
3
4// 인라인 모듈 스크립트, ok
5<script async type="module">
6  import { counter } from 'module1.js';
7  counter();
8</script>
9
10// 인라인 스크립트, bad
11<script async>
12  import { counter } from 'module1.js';
13  counter();
14</script>

모듈에 async를 사용하면 스크립트가 다운로드되고 실행되기 때문에 스크립트의 실행 순서에 영향을 받지 않는 광고나 모듈에 의존성이 없는 기능을 사용할 경우 적절하게 사용할 수 있습니다.


엄격 모드로 실행

모듈은 코드가 안전하고 예측 가능하게 실행될 수 있도록 항상 엄격 모드로 실행됩니다. 따라서 변수 선언 없이 변수에 값을 할당하거나 엄격 모드에서 오류라고 판단되는 코드는 제대로 동작하지 않습니다.

module.js
1undeclaredVariable = 'moonkorea'; // 에러
2
3function foo(x, x) {
4  // ..
5}

모듈 레벨 스코프

모듈은 모듈마다 자신의 스코프를 갖습니다. 따라서 모듈을 내보내고(export) 가져올 때(import) 정의되지 않은 변수나 함수 등의 엔티티에 접근하거나 스코프밖에 있는 개체를 사용할 수 없습니다.

index.html
1<script type="module">
2  let user = "John";
3</script>
4
5<script type="module">
6  alert(user); // 에러
7</script>

모듈 평가는 한 번만

내보내진 모듈을 여러 모듈에서 사용하더라도 최초 호출에 한 번만 실행됩니다.

module1.js
1alert('hello world');
module2.js
1import 'module1.js'; // 'hello world' 출력
module3.js
1import 'module1.js'; // 아무것도 실행되지 않음

위 코드에서는 두 개의 다른 모듈에서 module1을 가져오는데요, module2.js에서 가져온 모듈은 한 번 평가되고 alert 창이 출력되기 때문에 동일한 모듈을 가져오는 module3.js에서는 평가가 이루어지지 않습니다.

모듈의 이러한 특징은 모듈에서 내보내진 데이터 구조를 재사용할 때도 동일한데요,

user.js
1export let user = {};
2export function greet() {
3  alert(`${user.name}`);
4}
init.js
1import { user } from './user.js';
2user.name = 'moonkorea';
module1.js
1import { user, greet } from './user.js';
2alert(user.name); // 'moonkorea'
3greet(); // 'moonkorea'

최초 실행되는 모듈 init.js에서 user 변수에 name 프로퍼티를 추가합니다. user 변수를 사용하는 module1.js에서 user에 대한 정보를 사용해 값을 출력하면 최초 실행되는 모듈에서 변경된 값의 user가 사용되는 걸 확인할 수 있습니다.


모듈의 this 값

앞서 모듈은 엄격 모드에서 실행된다고 했는데요, 비엄격 모드에서 실행되는 일반 스크립트와 다르게 엄격 모드에서 this 값은 undefined입니다.

index.html
1<script>
2  alert(this); // window
3</script>
4
5<script type="module">
6  alert(this); // undefined
7</script>

출처 : 모듈 소개 - javascript.info