Modules

모듈은 서로 다른 사람이 만든 코드들을 합쳐 거대한 프로그램을 만들 수 있도록 돕습니다.

합치는 과정에서 다른 모듈 있는 코드가 내 코드가 영향을 주지 못하도록 제한합니다.

Modules with Classes, Objects, and Closures

클로저를 활용해 필요한 구현만 외부에 제공할 수 있습니다:

let stats = (function () {
  // 외부에 노출시키지 않는 함수
  const sum = (x, y) => x + y;
  const square = (x) => x * x;

  function mean(data) {
    return data.reduce(sum) / data.length;
  }

  function stddev(data) {
    let m = mean(data);
    return Math.sqrt(
      data
        .map((x) => x - m)
        .map(square)
        .reduce(sum) /
        (data.length - 1),
    );
  }

  // 외부에 노출하는 함수
  return { mean, stddev };
})();

console.log(stats.mean([1, 2, 3, 4, 5]));
console.log(stats.stddev([1, 2, 3, 4, 5]));

이와 같은 클로저 기반 모듈화는 자동화가 가능합니다. 웹팩 같은 번들러나 노드의 require가 이러한 방식을 사용합니다.

Modules in Node

파일을 네트워크에서 받아오는 브라우저와 달리 노드는 상대적으로 빠른 파일 시스템을 사용합니다. 따라서 굳이 프로그램을 하나의 파일로 합칠 필요가 없습니다.

노드에서 각 파일은 독립적인 네임스페이스를 가지는 독립된 모듈입니다. 상수, 변수, 함수 등을 export해야 다른 파일에서 볼 수 있어요.

노드에는 전역 객체인 moduleexports가 있고 이들을 통해 값들을 내보낼 수 있습니다. 두 객체는 조금씩 다릅니다:

stackoverflow.commodule.exports vs exports in Node.jsI've found the following contract in a Node.js module: module.exports = exports = nano = function database_module(cfg) {...} I wonder what's the difference between module.exports and exports and why

require로 값을 들여올 수 있습니다.

// 1. "/"가 없는 경우

// 노드의 빌트인 모듈 혹은
let fs = require('fs');
// 패키지 매니저로 설치한 모듈,
let express = require('express');

// 2. "/"가 있는 경우

// 내가 작성한 코드
let stats = require('./stats.js');

import, export가 도입되기 전까지는 브라우저에서도 번들러를 통해 require 문법을 많이 사용했습니다.

Modules in ES6

ESM 이전 스크립트에서는 top-level 선언들이 모든 스크립트간에 공유되지만, ESM에서는 파일 단위로 한정됩니다. ES6 모듈을 사용하면 자동으로 'use strict'가 되며 top-level에서의 thisundefined가 됩니다.

일반 export는 식별자가 있는 값에만 사용할 수 있지만 export default는 없어도 됩니다. import할 때 전자는 export한 이름으로 가져와야하지만 후자는 임의의 이름을 사용할 수 있습니다.

두 export 방법에 대한 논의:

github.comWhat is the benefit of prefer-default-export? · Issue #1365 · airbnb/javascriptThe docs don't have a Why section for prefer-default-export, and I'm not seeing the benefit of it on my own. I would think that not using default is preferred. With default exports, you lose refact...

ESM은 CJS와 다르게 top-level에서만 import/export가 가능하기에 정적 분석(tree-shaking 등)이 용이합니다. 또한 CJS는 로드한 모듈의 을 사용해 export쪽의 수정사항이 import쪽에 반영되지 않지만 ESM은 메모리 주소를 사용하기에 반영됩니다.

// 여러 객체 한 번에 export하기
export { Circle, degreesToRadians, PI };

// 모든 non-default export 가져오기
import * as stats from './stats.js';

// export 없는 모듈 사용하기
// 의미없어보이지만 이벤트 핸들러를 등록하는 등 유용한 작업을 할 수도 있어요
import './analytics.js';

// import하면서 이름 바꾸기
import { default as Histogram, mean, stddev } from './histogram-stats.js';

여러 모듈에 있는 export들을 모아 하나의 파일에서 제공하고 싶다면 re-exports를 활용합니다:

export { mean } from './stats/mean.js';
export { stddev } from './stats/stddev.js';

// 모든 named value들을 export합니다
export * from './stats/mean.js';
export * from './stats/stddev.js';

API의 중앙집중화, 코드 정리 등등을 위해 re-exports를 쓴다는데 성능에 좋지 않다는 글도 있네요:

x.comx.com

이제 대부분의 브라우저가 ESM을 지원해 번들러 없이도 개발이 가능하긴 하지만 장단점이 있습니다. 모듈을 활용해 스크립트들을 쪼개면 캐싱이 용이하지만 각 스크립트의 의존성을 해소하는 과정에서 waterfall이 일어날 수 있어요.

ESM을 쓰고싶으면 아래처럼 HTML의 script 태그에 type="module"을 명시합니다:

<script type="module">
  import './main.js';
</script>

타입이 모듈인 스크립트들은 defer인 스크립트들처럼 로드되고 실행됩니다. 즉 스크립트 태그를 만나자마자 필요한 모든 파일들이 로드되고 HTML 파싱 이후에 HTML 문서에 등장하는 순서대로 실행됩니다.

모듈은 기본적으로 defer됩니다. 아래 링크의 표를 참고해보세요:

v8.devJavaScript modules · V8This article explains how to use JavaScript modules, how to deploy them responsibly, and how the Chrome team is working to make modules even better in the future.

CJS와 ESM을 구분하기 위해 .mjs, .cjs 확장자를 쓰기도 해요:

stackoverflow.comWhat is the difference between .js and .mjs files?I have started working on an existing project based on Node.js. I was just trying to understand the flow of execution, where I encountered with some *.mjs files. I have searched the web where I found

모바일 환경에서 필요한 모든 모듈을 한 번에 불러오는 것은 비효율적이므로 dynamic import를 통해 번들을 쪼개 필요할 때 불러올 수 있습니다. 옛날에는 HTML에 스크립트 태그를 추가하는 방식으로 했었다네요.

async function analyzeData(data) {
  let stats = await import('./stats.js');
  return {
    average: stats.mean(data),
    stddev: stats.stddev(data),
  };
}

import.meta는 현재 실행중인 모듈의 메타데이터를 포함합니다. 그중에서도 url 값을 아래와 같이 활용할 수 있어요. 같은 디렉터리에 있는 다른 파일의 경로를 받아옵니다.

function localStringsURL(locale) {
  return new URL(`l10n/${locale}.json`, import.meta.url);
}

Summary

읽어볼만한 글들:

hacks.mozilla.orgES modules: A cartoon deep-dive – Mozilla Hacks - the Web developer blogES modules bring an official, standardized module system to JavaScript. With the release of Firefox 60 in May, all major browsers will support ES modules, and there is current work ...

stackoverflow.comWhat is the defined execution order of ES6 imports?I've tried searching the internet for the execution order of imported modules. For instance, let's say I have the following code: import "one" import "two" console.log("three"); Where one.js and ...

toss.techCommonJS와 ESM에 모두 대응하는 라이브러리 개발하기: exports fieldNode.js에는 두 가지 Module System이 존재합니다. 토스 프론트엔드 챕터에서 운영하는 100개가 넘는 라이브러리들은 그것에 어떻게 대응하고 있을까요?