Contents

ECMAScript Module과 Import

자바스크립트로 웹 페이지를 만들던, node.js 어플리케이션을 만들던 사용하게 되는 자바스크립트의 기능으로는 import가 있다.

모든 코드는 한 군데에 작성하는 것보다 기능적인 측면으로 묶어서 관리하기 위해 모듈화를 하게 되는데, FE 화면 개발을 하다보면 필연적으로 겪게 되는 개선 과제로도 ~ 공통 모듈화가 있다.

공통 모듈화를 진행하게 되면 코드의 재사용성은 물론, 가독성도 올라가므로 꽤 효과적인 방법으로 어지러운 코드를 리팩토링하는 방법인 것이다.

그럼, 이 모듈은 어떻게 관리되고 import를 사용해서 어떻게 불러와질까?

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

import

import 선언은 다른 모듈에 의해 export 된 read-only 라이브 바인딩을 불러오기 위해서 사용된다.

이렇게 불러와진 바인딩들은 ‘라이브 바인딩’이라고 불리는데

  • 바인딩을 export 한 모듈에 의해 업데이트 되지만
  • import 하는 모듈에 의해서 재할당 되지는 못하기 때문이다.

소스 파일에서 import 선언을 사용하기 위해서는 해당 파일이 먼저 런타임에 모듈로 해석되는지를 확인해야 한다.

  • HTML에서는 <script> 태그 안에 type="module"을 추가해서 모듈화 시킬 수있다.
  • 모듈들은 엄격 모드에서는 자동으로 해석된다.

type="module"을 사용하지 않는 보다 동적인 import()도 있다.

import 선언의 특징

import 선언은 다음 특징을 가진다.

  • 모듈들 안에서만 이루어질 수 있다.
  • 최상위 레벨에서만 가능하다. (블럭 안이나, 함수 안에서는 불가능하다)
  • import 선언이 모듈이 아닌 컨텍스트를 조우하게 되었을 때에는 SyntaxError가 발생한다.
    • type="module" 선언이 이루어지지 않은 <script>
    • “script"와 “function body"를 파싱 목표로 하는 eval, new Function` 등이 에시이다.
  • 모듈이 아닌 컨텍스트에서 모듈을 import하려면, 동적 import를 사용해야 한다.

import 선언들은 구문적으로 엄격하게 설계되어, 모듈이 평가되기 전에 정적으로 분석 / 링크될 수 있다.

  • 문자열 리터럴 specifier 이어야 한다.
  • 최상위 레벨에서만 실행되어야 한다.
  • 모든 바인딩은 식별자여야 한다.

이는 모듈을 본질적으로 비동기화 시켜서 최상위 수준에서의 await 기능을 지원하기 위함이다.

Named import

my-module 모듈에서 명시적으로 export 문을 사용해서 내보내어진 myExport라는 값이 주어지면, myExport는 현재 스코프에 포함된다.

import { myExport } from "/modules/my-module.js";

같은 모듈에서 동시에 여러 이름들을 불러올 수 있으며, import 당시에 rename도 가능하다. (as 문을 사용하여)

Default import

Default import는 기본 import 문법으로 불러와지는 경우에 해당한다.

import myDefault from "/modules/my-module.js";

default import는 기본적으로 이름을 명시적으로 지정하지 않기 떄문에, 원하는 어떤 이름이든 붙일 수 있다.

또한 default import는 namespace import나 named import와 같이 사용할 수 있다. 이러한 케이스에서는 default import가 우선적으로 선언되어야 한다.

Namespace import

다음 코드는 /modules/my-module.js에 있는 모든 모듈의 export를 포함하는 myModule을 현재 스코프에 추가한다.

import * as myModule from "/modules/my-module.js";

여기서 myModule은 모든 export를 속성으로 포함하는 네임스페이스 객체를 나타낸다. 상기 코드에서 가져온 모듈에 doAllTheAmazingThings() export가 포함된 경우, 다음과 같이 호출할 수 있다.

myModule.doAllTheAmazingThings();

모듈의 부가적인 요소만 불러오기

전체 모듈의 어떠한 것도 import 하지 않고, 오로지 부가적인 요소만 불러올 수도 있다.

이 경우, 모듈의 global 한 코드만 실행하고, 어떠한 값도 가져오지 않기 때문에 polyfill들에서 많이 사용된다.

import "/modules/my-module.js";

이런 기법이 polyfill에서 많이 사용되는 이유는, global 변수를 mutate 하기 때문이다.

webpack과 module

https://webpack.kr/guides/ecma-script-modules/

webpack에서는 파일이 ECMAScript Module (ESM)인지, 또는 다른 모듈 시스템인지를 자동으로 검사한다.

Node.js의 경우에는 package.json파일의 속성을 사용해서 파일의 모듈 유형을 명시적으로 설정할 수 있게 한다.

  • "type": "module" 선언이 되어 있다면 package.json 아래의 모든 파일이 ESM이 된다.
  • "type": "commonjs" 선언이 되어 있다면 CommonJS 모듈이 된다.

또한 파일의 확장자를 사용해서도 모듈의 유형을 설정할 수 있는데,

  • .mjs는 ESM이 되도록 강제한다.
  • .cjs는 CommonJS가 되도록 강제한다.

DataURI들에서 text/javascriptapplication/javascript MIME 타입으로 설정했다면 불러오는 모듈 유형은 자동으로 ESM으로 강제된다.

라이브러리 개발 시에 한번 더 생각해야 하는 점

https://toss.tech/article/commonjs-esm-exports-field

공통된 라이브러리를 개발하는 입장에서는 모듈을 어떻게 import 하던 불러온 모듈을 정상적으로 사용할 수 있어야 한다. 때문에 개발 할 때 ESM과 CommonJS 모듈을 둘 다 염두에 두고 개발해야 한다.

큰 차이점으로는 CJS Module loader가 동기적으로 작동하는 반면, ESM Module loader는 비동기적으로 작동하는 차이가 있다.

때문에 ESM에서 CJS를 import 할 수 있지만, CJS에서 ESM을 import할 수는 없다. CJS는 최상위 레벨에서의 await을 지원하지 않기 때문이다.

또 하나의 차이점으로는.. CJS는 tree shaking이 어려운 반면 ESM은 tree shaking이 용이하다는 면이 있다고 한다.

  • CJS는 빌드 타임에 정적으로 모듈을 분석하기 어렵다. 런타임에서만 모듈의 관계를 해석한다.
  • ESM은 정적인 구조로 모듈끼리 의존하도록 강제한다. 때문에 빌드 타임에 정적으로 모듈 간 의존 관계를 파악할 수 있기 때문에 tree shaking이 용이하다.