13장 모듈

2023.06.16

13.1 모듈 소개

  • 모든 규모의 프로젝트에서 프로그래머는 이름 충돌, 복잡한 종속성 및 코드를 적절한 크기의 파일로 분할하는 문제에 직면하게 된다.
  • 이러한 문제로 인해 CommonJS(cjs), 비동기식 모듈 정책 (Asynchronous Module Definition. AMD), 그 변형과 같은 코드 모듈을 정의하고 결합하기 위한 다양한 서로 다른 호환되지 않는 솔루션이 탄생했다.
  • ES2015는 자바스크립트용 모듈을 표준화하여 도구와 작성자가 사용할 수 있는 대부분의 공통 구문과 의미론을 제공한다.
  • 이전 모듈 방식의 이름 목록과 유사하게 기본 모듈은 CJS 모듈, AMD 모듈 또는 기타 유형과 구별되는 ESM 모듈(ECMAScript Module)이라고 하는 경우가 많다.

13.2 모듈 기초

  • 모듈은 자체 컴파일 단위(느슨하게는 "파일”)에 있는 코드 단위이다.
  • 모듈은 자체 스코프가 있다.
  • 다른 모듈에서 임포트를 통하여 다른 모듈을 로드할 수 있다.
  • 다른 모듈이 임포트한 엔트리를 익스포트할 수 있다.
  • 각각의 익스포트를 가져오는 모듈 그룹은 일반적으로 모듈 트리라고 하는 그래프를 형성한다.
  • 임포트할 때 모듈을 찾을 위치를 나타내는 문자열인 모듈 지정자(module specifier)를 사용하여 임포트할 모듈을 지정한다.
  • 자바스크립트 엔진은 호스트 환경과 함께 작동하여 지정한 모듈을 로드한다.
  • 자바스크립트 엔진은 영역(ex. 브라우저 창)당 한 번만 모듈을 로드한다.
  • 여러 다른 모듈에서 사용하는 경우 모두 동일한 복사본을 사용한다.
  • 모듈은 명명된 익스포트 및/또는 단일 기본 익스포트를 가질 수 있다.
  • 모듈의 코드는 전역 스코프에서 실행되지 않기 때문에 모듈은 전역 변수를 선언할 수 없다.
  • 익스포트를 임포트하면 임포트한 엔트리에 대한 읽기 전용 라이브 간접 바인딩(indirectbinding)이 생성된다.

13.2.1 모듈 지정자

import { example } from './mod.js';
  • 위의 예에서 ‘./mod.js’ 부분은 모듈 지정자이다.
  • 임포트 선언에서 모듈 지정자는 문자열을 생성하는 표현식이 아니라 문자열 리터럴이어야 한다.
  • 선언은 정적 양식이기 때문이다. 자바스크립트 엔진과 환경은 코드를 실행하지 않고도 이를 해석할 수 있어야 한다.

13.2.2 명명된 익스포트 기초

  • 명명된 익스포트를 사용하여 변수, 상수, 함수, 클래스 생성자, 다른 모듈에서 가져온 엔트리와 같이 이름이 있는 모든 모듈 내에서 선언된 모든 엔트리를 익스포트할 수 있다.
export let answer = 42;
export var question = 'Life';
export const author = 'coder';
export function fn() {}
export class Example {}
  • 익스포트 이름은 고유해야 한다.

13.2.3 기본 익스포트

  • 명명된 익스포트 외에도 모듈은 선택적으로 단일 기본 익스포트를 가질 수도 있다.
  • 기본 익스포트는 default라는 이름을 사용하는 명명된 익스포트와 같지만 고유한 전용 구문이 있으며 export 뒤에 키워드 default를 추가하여 기본 익스포트를 설정한다.
export default function example() {}
  • 모듈은 하나의 기본 익스포트만 가질 수 있다.
export default 6 * 7;
  • 위 기본 익스포트는 모듈 코드가 접근하는 데 사용할 수 있는 식별자가 없다는 점만 제외하면 let 익스포트와 똑같다.
  • 표현식을 평가하므로 let 익스포트와 마찬가지로 코드의 단계별 실행에서 익스포트 선언에 도달할 때까지 값이 존재하지 않는다. 그떄까지는 TDZ에 있다.

13.2.4 브라우저에서 모듈 사용하기

  • script 요소의 코드가 모듈임을 브라우저에 알리려면 type=”module”을 사용한다.
<script src='main.js' type='module'></script>
  • 모듈 스크립트는 구문 분석을 지연시키지 않는다.
  • 모듈과 그 종속성은 구문 분석과 병렬로 로드된다. 그런 다음 모듈의 코드는 구문 분석이 완료될 때 실행된다.
  • defer 속성과 차이점은 defer는 인라인 콘텐츠가 있는 스크립트 태그가 아니라 src 속성을 통해 외부 리소스에서 콘텐츠를 로드하는 스크립트만 지연시킨다. 그러나 type=”module”은 인라인 콘텐츠가 있는 스크립트도 연기한다.
  • 모듈의 코드는 HTML 구문 분석이 완료될 때까지 실행되지 않지만 브라우저와 자바스크립트 엔진은 함께 작동하여 모듈의 종속성을 확인하고(import 선언에서) 모듈이 의존하는 모든 모듈을 HTML 구문 분석과 병렬로 가져온다.
  • 모듈 지정자는 절대 URL이거나 상대 URL이다.
import /* .... */ 'http://example.com/a.js';
import /* .... */ '/a.js';
import /* .... */ './a.js';
import /* .... */ '../modules/d.js';
  • 많은 개발자가 확장자로 .js를 고수하지만, 일부는 파일에 스크립트뿐만 아니라 모듈이 포함되어 있다는 사실을 알리기 위해 .mjs를 선택하고 있다.
  • 모듈에는 고유한 MIME 유형이 없기 때문에 text/javascript로 파일을 제공하도록 웹 서버를 구성해야 한다.

13.2.5 노드제이에스에서 모듈 사용하기

  • 노드제이에스는 CJS 모듈 외에도 자바스크립트의 기본 모듈(ESM)을 지원한다.
  • ESM을 사용할 때 아래와 같이 세 가지 방법 중 하나를 설정해야 한다.
    • 프로젝트의 package.json의 type 필드를 “module” 값으로 만든다.
    • ESM 모듈 파일에서 .mjs를 파일 확장자로 사용한다.
    • 문자열을 노드에 전달할 때 —input-type=module을 지정한다.
  • package.json의 type 필드와 상관 없이 .cjs 확장자 파일을 임포트하면 해당 파일을 CJS 모듈로 로드한다.
  • 모듈 파일을 임포트할 때 노드제이에스의 모듈 지정자는 기본적으로 웹에 정의된 것과 매우 유사하다. URL이 아닌 절대 또는 상대 파일 이름이며 파일 확장자가 필요하다.
  • node_modules에 설치된 패키지를 임포트할 때 이름만을 사용한다.
import { writeFile } from 'fs';

13.3 익스포트를 다시 이름 짓기

  • 모듈에서 내보내는 식별자가 모듈 코드 내에서 사용하는 식별자와 같을 필요는 없다.
  • 익스포트 선언 내에서 as 절을 사용하여 익스포트 이름을 바꿀 수 있다.
let namewithinModule = 42:
export {exportedName as nameWithinModule };
  • exportName이라는 이름을 사용하여 다른 모듈에서 가져올 수 있다.
import { exportedName } from './mode.js';
  • nameWithinModule은 모듈 외부가 아니라 모듈 내에서만 사용하기 위한 것이다.
  • 이름 바꾸기 구문을 사용하여 기본 익스포트도 만들수 있지만 좋은 방법은 아니다.
export { expandStart as default };
  • 기본 익스포트는 default라는 익스포트 이름을 사용하는 명명된 익스포트와 같다.
export default expandStart;

13.4 다른 모듈의 익스포트를 다시 익스포트하기

  • 모듈은 다른 모듈의 익스포트를 다시 익스포트할 수 있다. 이를 간접 익스포트(indirect export)라고 한다.
export { example } from './example.js';
  • 이것이 유용한 곳은 롤업 모듈이다. 모든 코드가 사용되지 않음에도 모듈의 모든 코드를 메모리로 임포트하는 것을 방지하기 위해 필요한 부분만 임포트하도록 하는 것이다. ( 트리셰이킹)
  • 다른 모듈에 명명된 모든 익스포트를 익스포트하기라는 별표(*)를 사용하는 특별한 형태가 있다.
export * from './lib-a.js';
export * from './lib-b.js';
  • 이는 유지 보수시 더 편리함을 제공한다. ( 엔트리 추가시, 추가적인 업데이트 비용이 발생하지 않으므로 )

13.5 임포트를 다시 이름 짓기

  • as 절을 사용하여 임포트 이름을 바꿀 수 있다.
import { example as aExample } from './a.js';
import { example as bExample } from './b.js';
  • 기본 익스포트를 임포트할 때 항상 고유한 이름을 선택하므로 as 절은 관련이 없다.
import someName from '/.mod.js';
import someOtherName from '/.mod.js';

console.log(someOtherName === someName); // true
  • export의 이름 바꾸기 양식과 마찬가지로 import의 이름 바꾸기 양식을 사용하여 원하는 경우 기본 익스포트를 임포트할 수도 있지만 모범 사례는 아니다.
import { default as someOtherName };

13.6 모듈의 네임스페이스 객체 임포트하기

  • 개별 익스포트를 임포트하는 대신 전체 모듈에 대한 모듈 네임스페이스 객체를 임포트할 수 있다.
// module.js
export function example() {
  console.log('example called');
}
export const something = 'something';
export default function () {
  console.log('default called');
}

// use.js
import * as mod from './module.js';
mod.example(); // example called
mod.something; // something
mod.default(); // default called
  • 모듈 네임스페이스 객체는 모듈과 동일하지 않다. 모듈의 모든 익스포트에 대한 속성을 사용하여 처음 요청될 때 생성되는 별도의 객체이다.
  • 모듈 네임스페이스 객체가 생성되면 다른 모듈이 모듈의 네임스페이스 객체를 가져오는 경우에도 재사용된다.
  • 객체는 읽기 전용이며, 속성을 쓰거나 추가할 수 없다.

13.7 다른 모듈의 네임스페이스 객체 익스포트하기

  • ES2020의 규범적 변경에 따라 모듈은 다른 모듈의 네임스페이스 객체를 임포트해서 익스포트를 할 수 있다.
export * as stuff from './module2.js';

13.8 단지 사이드 이펙트를 위해 모듈 임포트하기

  • 모듈에서 아무것도 가져오지 않고 모듈을 로드하고 실행하기 위해 임포트할 수 있다.
import './mod.js';
  • mod.js를 로드하면 최상위 코드가 실행되므로, 이는 최상위 코드에 있을 수 있는 부작용에 대해 모듈만 임포트할 때 유용하다.
  • 일반적으로 모듈의 최상위 코드에 부작용이 없는 것이 가장 좋지만 예외를 만드는 것이 적절한 경우에 따라 모듈의 최상위 코드를 실행하게 하는 방법을 제공한다.

13.9 임포트와 익스포트 엔트리

  • 자바스크립트 엔진이 모듈을 구문 분석할 때 모듈의 임포트 엔트리 목록과 익스포트 엔트리 목록을 작성한다.

13.9.1 임포트 엔트리

  • 임포트 엔트리에는 세 개의 필드가 있다.
    • [[ModuleRequest]]: 임포트 선언의 모듈 지정자 문자열이며 임포트가 어떤 모듈에서 오는지 알려준다.
    • [[ImportName]]: 임포트한 것의 이름이며 모듈 네임스페이스 객체를 임포트하는 경우 * 이다.
    • [[LocalName]]: 임포트한 엔트리에 사용할 로컬 식별자(바인딩)의 이름이다. 종종 [[ImportName]]과 동일하지만 임포트 선언에서 as를 사용하여 임포트 이름을 바꾼 경우에는 다를 수 있다.
| 임포트 명령문 양식             | [[ModuleRequest]]                     | [[ImportName]] | [[LocalName]] |
| ------------------------------ | ------------------------------------- | -------------- | ------------- |
| `import v from 'mod';`         | "mod"                                 | `default`      | `v`           |
| `import * as ns from 'mod';`   | "mod"                                 | `*`            | `ns`          |
| `import { x } from 'mod';`     | "mod"                                 | `x`            | `x`           |
| `import { x as v} from 'mod';` | "mod"                                 | `x`            | `v`           |
| `import 'mod';`                | ImportEntry 레코드가 생성되지 않는다. |

13.9.2 익스포트 엔트리

  • 익스포트 엔트리에는 네 개의 필드가 있다.
    • [[ExportName]]: 익스포트의 이름이다. 임포트할 때 다른 모듈이 사용하는 이름이다. 기본 익스포트에 대한 문자열 "default"이다. 다른 모듈에서 모든 것을 다시 익스포트하는 export * from 선언의 경우 null이다.
    • [[LocalName]]: 익스포트하는 로컬 식별자(바인딩)의 이름이다. 이것은 종종 [[ExportName]]과 동일하지만 익스포트 이름을 변경한 경우 다를 수 있다. 관련된 로컬 이름이 없기 때문에 이 엔트리가 다른 모듈의 익스포트를 다시 익스포트하는 export .... from 선언에서 익스포트를 위한 것이라면 이것은 null이다.
    • [[ModuleRequest]]: 다시 익스포트하는 경우 export ... from 선언의 모듈 지정자 문자열이다. 모듈 자체 익스포트의 경우 null이다.
    • [[ImportName]]: 다시 익스포트하는 경우 다시 익스포트할 다른 모듈의 익스포트 이름이다. 종종 이것은 [[ExportName]]과 동일하지만 다시 익스포트 선언에서 as 절을 사용한 경우 다를 수 있으며, 모듈 자체 익스포트의 경우 null이다.
    | 익스포트 명령문 양식              | [[EXPORTNAME]] | [[ModuleRequest]] | [[ImportName]] | [[LocalName]] |
    | --------------------------------- | -------------- | ----------------- | -------------- | ------------- |
    | `export var v;`                   | `v`            | null              | null           | `v`           |
    | `export default function f() {};` | `default`      | null              | null           | `f`           |
    | `export default function () {};`  | `default`      | null              | null           | `*default*`   |
    | `export default 42;`              | `default`      | null              | null           | `*default*`   |
    | `export { x };`                   | `x`            | null              | null           | `x`           |
    | `export { v as x };`              | `x`            | null              | null           | `v`           |
    | `export { x } from 'mod';`        | `x`            | "mod"             | `x`            | null          |

13.10 임포트는 살아있고 읽기 전용이다

  • 모듈에서 무언가를 임포트할 때 원본 엔트리에 대한 간접 바인딩이라고 하는 읽기 전용 라이브 바인딩을 얻는다.
  • 읽기 전용이므로 코드에서 바인딩에 새 값을 할당할 수 없다. 하지만 라이브 바인딩이기 때문에 코드는 원래 모듈이 할당한 새 값을 볼 수 있다.
// mode.js
const a = 1;
let c = 0;
export { c as counter };
export function increment() {
  ++c;
}

// main.js
import { counter, increment as inc } from './mod.js';
console.log(counter); // 0
inc();
console.log(counter); // 1
counter = 42; // TypeError: Assignment to constant variable.
  • main.js의 코드는 mod.js가 수행하는 카운터 변경 사항을 확인하지만 값을 직접 설정할 수는 없다.
  • 각 모듈에는 모듈 환경 객체가 있다. 모듈 환경 객체에는 해당 모듈의 모든 익스포트와 임포트에 대한 바인딩이 있다.

13.11 모듈 인스턴스는 영역 전용이다

  • 모듈은 영역(창, 탭, 워커 등)당 한 번만 로드된다.
  • 특히 자바스크립트 엔진은 영역 내에서 로드된 모듈을 추적하고 두 번 이상 요청된 모듈을 재사용한다. 거꾸로 말하면 영역이 다르면 모듈 인스턴스도 다르다.
  • 다른 영역은 모듈 인스턴스를 공유하지 않는다.
  • 모듈은 영역 간에 공유되지 않고 영역 내에서만 공유된다.

13.12 어떻게 모듈을 읽어 오는가?

  • 자바스크립트 모듈 시스템은 간단한 사용 사례를 단순하게 유지하지만 복잡한 사용 사례를 잘 처리하도록 설계되었다. 이를 위해 모듈은 세 단계로 로드된다.
    • 임포트와 구문 분석: 모듈의 소스 텍스트를 임포트하고 구문 분석하여 임포트 및 익스포트를 결정한다.
    • 인스턴스화: 모든 임포트 및 익스포트에 대한 바인딩을 포함하여 모듈의 환경 및 바인딩을 생성한다.
    • 평가: 모듈의 코드를 실행한다.

13.12.1 임포트와 구문 분석

  • 모듈이 아닌 스크립트와 달리 모듈 스크립트는 HTML 파서를 멈추게 하지 않으므로 HTML 파서는 모듈을 파싱하고 로드하는 작업이 완료되는 동안 계속된다.
  • 특히 모듈 스크립트는 기본적으로 지연된다(defer 속성이 있는 것처럼).
  • 이는 HTML 파서가 페이지 구문 분석을 완료할 때까지 해당 코드가 평가되지 않음을 의미한다.
  • HTML 파서가 완료되기 전에도 평가가 가능한 한 빨리 발생하도록 하려면 대신 async를 지정할 수 있다.
  • 브라우저가 entry.js의 내용을 자바스크립트 엔진에 전달할 때 엔진은 이를 구문 분석하고 이에 대한 모듈 레코드를 생성한다.
  • 모듈 레코드에는 구문 분석된 코드, 이 모듈에 필요한 모듈 목록, 임포트 엔트리익스포트 엔트리 목록, 모듈상태(로드와 평가 과정에 있음)가 들어간다.
<img className='w-8/12 m-auto' src='/assets/images/web-javascript-1.jpg' />
<br />
  • 자바스크립트 엔진은 해당 모듈 레코드를 호스트에 반환하고, 호스트는 이를 완전히 확인된 모듈 지정자 아래의 확인된 모듈 맵에 저장한다.
    • 나중에 모듈 레코드가 필요할 때 자바스크립트 엔진은 호스트에게 요청한다.\
    • 호스트는 모듈 맵에서 찾아보고 발견하면 반환한다.
    • 이 시점에서 entry.js가 임포트해서 구문 분석되었으므로 자바스크립트 엔진은 entry.js의 인스턴스화 단계를 시작한다.
    • 기본적으로 가장 먼저 하는 일은 브라우저에 modl js와 mod2.js를 확인하도록 요청하는 것이다. 임포트하여 구문 분석되면 브라우저에는 세 가지 모듈 모두에 대한 엔트리가 있는 모듈맵이 있다.
    • 모듈 레코드의 정보를 통해 자바스크립트 엔진은 인스턴스화와 평가해야 하는 모듈 트리를 결정할 수 있다. 트리는 아래 그림과 같다.
<img className='w-8/12 m-auto' src='/assets/images/web-javascript-2.jpg' />
<br />

13.12.2 인스턴스화

  • 이 단계에서 자바스크립트 엔진은 모듈의 모든 임포트와 익스포트에 대한 바인딩을 포함하여 각 모듈의 환경 객체와 그 안에 최상위 바인딩을 만든다.
    • 로컬의 경우 직접 바인딩이다.
    • 임포트의 경우 엔진이 익스포트를 위한 익스포트 모듈의 바인딩에 연결하는 간접 바인딩이다.
  • 인스턴스화는 최하위 모듈이 먼저 인스턴스화되도록 깊이 우선 탐색을 사용하여 수행된다.
  • 모듈을 인스턴스화하면 해당 범위가 생성되지만 해당 코드는 실행되지 않는다. 즉, 호이스트 가능한 선언이 처리되어 그에 대한 함수가 생성되지만 어휘 바인딩은 초기화되지 않은 상태로 남는다. 이러한 선언은 TDZ에 있다.

13.12.3 평가

  • 이 단계에서 자바스크립트 엔진은 모듈의 최상위 코드를 다시 깊이 우선 순서로 실행하여 각 모듈을 "평가됨"으로 표시한다.
  • 코드가 실행되면 모든 최상위 초기화되지 않은 바인딩이 코드 실행에 도달하면 초기화된다.
  • 자바스크립트 모듈 시스템은 각 모듈의 최상위 코드가 한 번만 실행되도록 한다.
  • 브라우저에서 엔트리 포인트에 대한 script 태그에 async 속성이 없으면 HTML 파서가 문서 구문 분석을 완료할 때까지 평가가 시작되지 않는다.
  • async 속성이 있으면 HTML 파서가 문서에서 계속 작업 중이더라도 인스턴스화가 완료된 후 최대한 빨리 평가가 시작된다.

13.12.4 임시 데드존(TDZ)를 정리하며

  • TDZ는 호이스트 가능한 선언이 아니라 어휘 바인딩에만 관련된다.
    • 호이스트 가능한 선언은 var 선언, 함수 선언이 있다.
  • TDZ는 환경 객체가 생성되고 모듈에 대한 환경을 포함하여 해당 바인딩을 임포트할 때마다 적용된다.
  • 모듈에 대한 환경 객체는 모듈 인스턴스화 중에 생성되고 해당 코드는 나중에 모듈 평가 중에 실행된다.
  • 모듈이 최상위 범위에 있는 모든 어휘 바인딩은 인스턴스화부터 평가 중에 선언에 도달할 때까지 TDZ에 있다.

13.12.5 순환 종속성과 TDZ

// entry.js
import { fn1 } from './mod1.js';
import def, { fn2 } from './mod2.js';

fn1();
fn2();
def();

// mod1.js
import def from './mod2.js';

const indentString = '  ';

export function indent(nest = 0) {
  return indentString.repeat(nest);
}

export function fn1(nest = 0) {
  console.log(`${indent(nest)}mod1 - fn1`);
  def(nest + 1);
}

// mod2.js
import { fn1, indent } from './mod1.js';

export function fn2(nest = 0) {
  console.log(`${indent(nest)}mod2 - fn2`);
  fn1(nest + 1);
}

export default function (nest = 0) {
  console.log(`${indent(nest)}mod2 - default`);
}
  • 위의 예에서 mod1.js와 mod2.js는 서로를 참조한다. 즉 순환 종속성에 있다.
  • 모듈을 로드하고 평가하는 3단계 프로세스 때문에 자바스크립트 모듈 시스템에서는 문제가 되지 않는다.
  • mod1.js, mod2.js 모두 함수 호출에 대한 응답으로 임포트한 것만 사용한다.
  • 아래와 같이 mod2.js를 변경하면 에러가 발생한다.
import { fn1, indent } from './mod1.js';

console.log(`${indent(0)}hi there`); // ReferenceError

export function fn2(nest = 0) {
  console.log(`${indent(nest)}mod2 - fn2`);
  fn1(nest + 1);
}

export default function (nest = 0) {
  console.log(`${indent(nest)}mod2 - default`);
}
  • mod2.js는 mod1.js가 평가되기 전에 indent를 사용하려고 하기 때문이다.

13.13 임포트/익스포트 문법를 정리하며

13.13.1 다양한 방식의 익스포트

  • 다양한 방식의 익스포트 양식에 대한 요약이다.
export let a;
export let b = '2';
export function f() {}
export class G(){}

export { a };
export { b };
export { f as fFunction };
export { g, f };

export * from './mod.js';

// 기본 익스포트는 하나만 가능하다.
export default var h = 'hh';
export default function(){}
export default class k(){}

13.13.2 다양한 방식의 임포트

  • 다양한 방식의 익스포트 양식에 대한 요약이다.
import { example } from './mod.js';
import { example as e } from './mod.js';

import * as mod from './mod.js';

// 기본 임포트
import example from './mod.js';
import example, { exampleSecond } from './mod.js';

// 익스포트를 임포트하지 않고 부작용만을 위한 임포트
import './mod.js';

13.14 동적 임포트

  • 일부 사용 사례에서는 모듈이 런타임에 가져올 엔트리나 위치를 결정해야 한다.
  • ES2020은 동적 임포트를 추가했다.

13.14.1 동적으로 모듈 임포트

  • 동적 임포트는 import가 함수인 것처럼 호출할 수 있는 새로운 구문을 추가한다. 호출되면 import는 모듈의 네임스페이스 객체에 대한 프라미스를 반환한다.
import(/* ... 일부 런타임-결정 이름 */)
  .then(ns => /* ... */)
    .catch(err => /* ... */)
  • 함수 호출처럼 보이지만 import(…)는 함수 호출이 아니다. importCall이라는 새로운 구문이다.
    • 호출은 일반 함수 호출이 전달하지 않는 컨텍스트 정보를 전달해야 한다.
    • 앨리어싱을 허용하지 않으면 정적 분석이 모듈에서 동적 임포트가 사용되는지 여부를 알 수 있다.
    // 동작하지 않음.
    const imp = import
    imp(/* ... 일부 런타임-결정 이름 */)
      .then(ns => /* ... */)
        .catch(err => /* ... */)
  • 동적으로 로드된 모듈은 정적으로 로드된 모듈과 마찬가지로 캐시된다.

13.14.2 동적 모듈 예

// dynamic-mod-log.js
log('log module evaluated');
export function log(msg) {
  const p = document.createElement('pre');
  p.appendChild(document.createTextNode(msg));
  document.body.appendChild(p);
}

// dynamic-mod1.js
import { log } from './dynamic-mod-log.js';
import { showTime } from './dynamic-mod-showtime.js';

log('dynamic module number 1 evaluated');
export function example(logFromEntry) {
  log('Number 1! Number 1! Number 1!');
  log(`log === logFromEntry? ${log === logFromEntry}`);
  showTime();
}

// dynamic-mod2.js
import { log } from './dynamic-mod-log.js';
import { showTime } from './dynamic-mod-showtime.js';

log('dynamic module number 2 evaluated');
export function example(logFromEntry) {
  log("Meh, being Number 2 isn't that bad");
  log(`log === logFromEntry? ${log === logFromEntry}`);
  showTime();
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Dynamic Module Loading Example</title>
</head>
<body>
<script src="dynamic-example.js" type="module"></script>
</body>
</html>

// dynamic-example.js
import { log } from "./dynamic-mod-log.js";

log("entry point module top-level evaluation begin");
(async () => {
  try {
    const modName = `./dynamic-mod${Math.random() < 0.5 ? 1 : 2}.js`;
    log(`entry point module requesting ${modName}`);
    const mod = await import(modName);
    log(`entry point module got module ${modName}, calling mod.example`);
    mod.example(log);
  } catch (error) {
    console.error(error);
  }
})();
log("entry point module top-level evaluation end");

13.14.3 비모듈 스크립트의 동적 임포트

  • 정적 임포트와 달리 동적 임포트는 모듈뿐만 아니라 스크립트에서도 사용할 수 있다.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Dynamic Module Loading in Script - Example</title>
</head>
<body>
<script src="dynamic-example-2.js"></script>
</body>
</html>

// dynamic-example-2.js
(async () => {
  try {
    const { log } = await import("./dynamic-mod-log.js");
    log("entry point module got log");
    const modName = `./dynamic-mod${Math.random() < 0.5 ? 1 : 2}.js`;
    log(`entry point module requesting ${modName}`);
    const mod = await import(modName);
    log(`entry point module got module ${modName}, calling mod.example`);
    mod.example(log);
  } catch (error) {
    console.error(error);
  }
})();
  • 엔트리 포인트와 로그 모듈 간의 관계를 더 이상 정적 분석을 통해 결정할 수 없다.
  • 로그 모듈에서 이름이 지정된 log를 임포트하는 대신 동적 버전이 로그 모듈에 대한 전체 네임스페이스 객체를 임포트한다.
  • 대조적으로 정적 예에서는 모듈 네임스페이스 객체가 생성되지 않는다. 아무것도 요구하지 않기 때문이다.

13.15 트리 셰이킹

  • 트리 셰이킹(tree shaking)은 데드 코드 제거(dead code elimination)의 한 형태이다.
  • 트리 셰이킹은 모듈 트리를 분석하여 데드 코드를 제거하는 프로세스이다.
  • 트리 셰이킹은 브라우저의 모듈에 대한 기본 지원에도 불구하고 가까운 장례에 자바스크립트 번들러가 사라지지 않는 한 가지 이유이다.
  • 트리의 모든 모듈에서 문자열 리터럴 이외의 다른 것과 함께 import()를 한 번만 사용하더라도 도구는 실제로 사용되지 않는 것을 증명할 수 없기 때문에 트리 셰이킹을 수행할 수 없다.
  • 다른 번들러는 다양한 방식으로 동적 임포트의 영향을 최적화하고 트리 셰이크를 시도할지 여부와 시도 방법을 제어할 수 있는 구성 옵션을 제공할 수 있다.

13.16 번들링

  • 모듈은 현재 최신 브라우저에서 기본적으로 지원되지만 사람들은 모듈을 지원하지 않는 브라우저에서 작동하는 최적화된 파일로 모듈을 변환하기 위해 자바스크립트 번들러를 사용하여 한동안 구문을 사용해 왔다.
  • 모든 규모의 프로젝트에서 기본 모듈 지원이 있는 브라우저만 대상으로 하는 경우에도 번들러를 사용하고 싶을 것이다. 트리 셰이킹도 한 가지 이유이다.
  • 다른 하나는 HTTP(심지어 HTTP/2 포 함)를 통해 모든 개별 모듈 리소스를 전송하는 것이 단일 리소스를 전송하는 것보다 여전히 잠재 적으로 느리다(HTTP/1.1만 사용하는 경우 확실히 느리다).
  • 구글은 두 개의 인기 있는 실제 라이브러리(Moment.js와 Three.js)와 합성적으로 생성된 많은 모듈을 사용하여 기본 모듈 로딩과 로딩 번들을 비교하여 번들링이 여전히 유용한지 여부와 크롬 로딩 파이프라인에서 병목 현상이 발생한 위치를 확인하는 분석을 수행했다.
  • 분석 결과를 바탕으로 구글의 애디 오스마니와 마티스 바이넌스는 다음을 권장한다.
    • 개발 중에는 기본 지원을 사용한다.
    • 최신 브라우저만 대상으로 하는 100개 미만의 모듈과 얕은 종속성 트리(최대 깊이 5 미만)가 있는 소규모 웹 앱에 대해 프로덕션 환경에서 기본 지원을 사용한다.
    • 그보다 큰 제품이나 모듈 지원이 없는 브라우저를 대상으로 하는 프로젝트를 위해 번들링을 사용한다.
  • 어떤 이유로든 HTTP 1.1을 계속 사용해야 하는 경우에는 어느 쪽이든 장단점이 있다.

13.17 메타데이터 임포트하기

  • 경우에 따라 모듈은 로드된 URL 또는 경로, "주" 모듈인지 등과 같은 자체 정보를 알아야 할 수도 있다. -
  • ES2015의 모듈은 현재 모듈이 해당 정보를 얻을 수 있는 방법을 제공하지 않는다. ES2020은 모듈이 자신에 대한 정보를 얻을 수 있는 방법을 추가했다.
  • 바로 import.meta이다. import.meta는 모듈에 대한 속성을 포함하는 모듈 고유의 객체이다.
  • 속성 자체는 호스트 지정이며 환경(예: browser or node.js)에 따라 다르다.
  • 웹 환경에서 import.meta의 속성은 HTML 사양의 HostGetImportMetaProperties 절에 의해 정의된다. 현재 url이라는 단일 속성만 정의되어 있다.
  • node.js는 모듈의 절대 경로인 URL을 제공하여 url을 지원한다.
  • 시간이 지남에 따라 환경 중 하나 또는 둘 모두에 더 많은 속성이 추가될 것이다.

13.18 워커 모듈

  • 기본적으로 웹 워커는 모듈이 아닌 클래식 스크립트로 로드된다.
  • 워커는 importscripts를 통해 다른 스크립트를 로드할 수 있지만 로드 스크립트와의 통신이 워커의 전역 환경을 통해 이루어지는 구식 방식이다. 그러나 웹 워커는 모듈이 될 수 있으므로 임포트와 익스포트 선언을 사용할 수 있다.

13.18.1 웹 워커를 모듈로 로드하기

  • 워커를 모듈로 로드하여 모듈이 되는 것의 모든 일반적인 이점을 얻으려면 워커 생성자에 두 번째 인수로 전달된 options 객체의 type 옵션을 사용한다.
const worker = new Worker('/worker.js', { type: 'module' });
  • 모듈 또는 클래식 스크립트 내에서 이 방법으로 워커를 시작할 수 있다. 또한 기존 스크립트 워커와 달리 교차 출처(cross-origin)에서 사용할 수 있도록 하는 교차 출처 리소스 공유(CORS) 정보가 제공되는 경우 모듈 워커를 교차 출처로 실행할 수 있다.
  • 브라우저가 워커를 모듈로 지원하는 경우 워커는 모듈로 로드된다.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Web Worker Module Example</title>
  </head>
  <body>
    <script>
      const worker = new Worker("./worker-example-worker.js", {
        type: "module",
      });
      worker.addEventListener("message", (e) => {
        console.log(`Message from worker: ${e.data}`);
      });
    </script>
  </body>
</html>

// worker-example-worker.js
import { example } from "./worker-example-mod.js";

postMessage(`example(4) = ${example(4)}`);

// worker-example-mod.js
export const example = a => a * 2;

13.18.2 노드제이에스 워커를 모듈로 로드하기

  • ESM 모듈에 대한 노드제이에스의 지원은 워커 스레드로 확장된다.
  • 코드를 실행하는 다른 방법과 동일한 규칙이 적용된다. 가장 가까운 package.json 파일에서 type: "module" 설정을 사용하거나 워커에게 .mjs 확장자를 지정하여 워커를 모듈로 시작할 수 있다.

13.18.3 워커는 자신의 영역에 있다

  • 워커가 생성되면 새 영역에 배치된다. 자체 전역 환경, 고유한 객체 등이 있다.
  • 워커가 모듈인 경우 로드하는 모듈은 로드된 모듈과 별도로 해당 영역 내에서 로드된다.
  • 예를 들어, 브라우저에서 기본 창의 모듈이 mod1.js를 로드하고 mod1.js도 로드하는 워커를 시작하는 경우 mod1.js는 기본 창 영역에서 한 번, 워커 영역에서 다시 두 번 로드된다.
  • 이것은 웹 브라우정와 노드제이에스 모두에 해당된다.

13.19 과거 습관을 새롭게

13.19.1 의사 네임스페이스 대신 모듈 사용하기

  • 모듈을 사용하자.
function privateFunction() {
  // ...
}

export function publicFunction() {
  // ...
}

13.19.2 스코프 지정 함수에서 코드를 래핑하는 대신 모듈을 사용하기

  • 모듈의 최상위 범위가 전역 스코프가 아니므로 모듈을 사용하자.
let x = /* ... */;
function doSomething() {
  // ...
}
// ...

13.19.3 모듈을 사용하여 거대한 코드 파일 생성 방지하기

  • 정적으로 선언된 종속성과 함께 적절한 크기의 다양한 모듈을 대신 사용하자.
  • 어떤 크기가 올바른 크기인지에 대한 질문은 스타일의 문제이며 팀과 동의해야 한다.
  • 각 기능을 자체 모듈에 넣는 것은 아마도 과잉이지만, 이것, 저것, 그리고 다른 하나를 내보내는 단일 대규모 모듈은 아마도 충분히 모듈화되지 않을 것이다.

13.19.4 CJS, AMD 및 기타 모듈을 ESM으로 변환하기

  • ESM을 사용하여 기존 모듈을 변환하자.

13.19.5 자작으로 만드는 것보다 잘 관리된 번들러를 사용하자

  • 잘 관리되고 커뮤니티가 지원되는 번들러를 사용하자.