프론트엔드 개발자들은 주로 webpack, rollup(vite)에 의존한 모듈 resolution을 자연스럽게 사용하기에,
브라우저에서 ESM을 직접 사용하면, 생각했던 대로 동작하지 않는다는 사실을 알 수 있습니다.
ECMAScript의 모듈 해석 알고리즘을 알아봅시다.
specifier(지정자)를 사용한 모듈 참조
// Static import
import {namedExport} from 'https://example.com/some-module.js'; // (A)
console.log(namedExport);
// Dynamic import
import('https://example.com/some-module.js') // (B)
.then((moduleNamespace) => {
console.log(moduleNamespace.namedExport);
});
- 라인 A의 from 이후 문자열
- 라인 B의 문자열 인수
- 절대 지정자(absolute specifier) : 전체 URL
- 절대 지정자는 주로 웹에서 직접 호스팅되는 라이브러리에 액세스하는 데 사용됩니다.
- file:///opt/nodejs/config.mjs
- https://www.unpkg.com/browse/yargs@17.3.1/browser.mjs
- 절대 지정자는 주로 웹에서 직접 호스팅되는 라이브러리에 액세스하는 데 사용됩니다.
- 상대 지정자(relative specifier) : 상대 URL('/', './' 또는 '../'로 시작)입니다.
- 상대 지정자는 대부분 동일한 코드 베이스 내의 다른 모듈에 액세스하는 데 사용됩니다.
- ./sibling-module.js
- ../module-in-parent-dir.mjs
- ../../dir/other-module.js
- 상대 지정자는 대부분 동일한 코드 베이스 내의 다른 모듈에 액세스하는 데 사용됩니다.
모든 모듈에는 프로토콜이 해당 리소스의 위치(file:, https: 등)에 따라 달라지는 URL이 있습니다.
상대 지정자를 사용하는 경우 JavaScript는 해당 지정자를 사용해 모듈의 URL을 확인하여 프로토콜을 포함한 전체 URL로 바꿉니다.
- 기본 지정자(bear specifier) :
- 슬래시나 점으로 시작하지 않는 경로(프로토콜 및 도메인 없음)입니다.
- 패키지 이름으로 시작합니다.
- 해당 이름 뒤에는 선택적으로 하위 경로가 올 수 있습니다.
- some-package
- some-package/sync
- some-package/util/files/hello-world.js
- 기본 지정자는 scoped name(범위가 지정된 이름을 가진 패키지 명)을 참조할 수 있습니다.
- @some-scope/scoped-name
- @some-scope/scoped-name/async
파일 확장자와 모듈 지정자
절대 지정자와 상대 지정자는 항상 파일 이름 확장자를 가집니다(일반적으로 .js 또는 .mjs).
기본 지정자(bear specifier)에는 세 가지 스타일이 있습니다.
- 스타일 1: 하위 경로 없음
- 스타일 2: 파일 이름 확장자가 없는 하위 경로. 이 경우 하위 경로는 패키지 이름에 대한 수정자(modifier)처럼 작동합니다.
- my-parser/sync
- my-parser/async
- 스타일 3: 파일 이름 확장자가 있는 하위 경로. 이 경우 패키지는 모듈 컬렉션으로 표시되며 하위 경로는 그 중 하나를 가리킵니다.
- large-package/misc/util.js
- large-package/main/parsing.js
- large-package/main/printing.js
모듈 해석 알고리즘
모듈 지정자에 대해 알아보았으니, node.js가 어떻게 모듈을 가져오는지 알아봅시다.
- 매개변수:
- import 대상 모듈의 URL
- 모듈 지정자
- 해석 결과: 모듈 지정자의 해석된 URL
알고리즘은 다음과 같습니다.
- 절대 지정자는 이미 해석이 완료된 것입니다. 세 가지 프로토콜이 가장 일반적입니다.
- file : 로컬 파일
- https : 리모트 파일
- node : node.js 빌트인 모듈
- 상대 지정자의 경우 import 대상 모듈의 URL을 이용해 해석합니다.
- 기본 지정자의 경우 package export의 약어입니다
- '#'으로 시작하는 경우 패키지 가져오기(Package imports)를 이용해 해석합니다
- 그렇지 않으면 기본 지정자는 다음 중 하나입니다(하위 경로는 선택 사항임).
- «package»/sub/path
- @«scope»/«scoped-package»/sub/path
- 해석 알고리즘은 기본 지정자의 시작 부분과 일치하는 하위 디렉터리가 있는 node_modules 디렉터리를 찾을 때까지 현재 디렉터리와 상위 디렉터리를 순회합니다. 즉, 다음 중 하나입니다.
- node_modules/«package»/
- node_modules/@«scope»/«scoped-package»/
- 기본적으로 패키지 ID 뒤의 (잠재적으로 비어 있는) 하위 경로는 패키지 디렉토리에 상대적인 것으로 해석됩니다.
- 기본값은 패키지 내보내기(package export)를 통해 재정의할 수 있습니다.
- 기본 지정자의 경우 파일이 아닌 패키지 단위의 export를 의미합니다.
- 모듈 파일은 일반적으로 다음과 같은 파일 이름 확장자를 갖습니다.
- 이름 확장명이 .js인 파일은 가장 가까운 package.json에 다음 항목이 있는 경우 ES 모듈입니다.
- "type": "module"
Node.js가 stdin, --eval 또는 --print를 통해 제공된 코드를 실행하는 경우
ES 모듈로 해석되도록 다음 명령줄 옵션(the following command-line option)을 사용합니다.
--input-type=module
패키지 내보내기 : 다른 패키지가 바라볼 것 컨트롤하기
my-lib/
dist/
src/
main.js
util/
errors.js
internal/
internal-module.js
test/
- 패키지 내부 숨기기:
- "exports" 속성이 없으면 my-lib 패키지의 모든 모듈은 패키지 이름 뒤의 상대 경로를 통해 액세스할 수 있습니다. 예:
- my-lib/dist/src/internal/internal-module.js
- 속성이 존재하면 속성에 나열된 지정자만 사용할 수 있습니다. 다른 모든 것은 외부에 숨겨져 있습니다.
- "exports" 속성이 없으면 my-lib 패키지의 모든 모듈은 패키지 이름 뒤의 상대 경로를 통해 액세스할 수 있습니다. 예:
- 더 나은 모듈 지정자: 패키지 내보내기를 통해 더 짧거나 더 나은 이름을 가진 모듈의 기본 지정자 하위 경로를 정의할 수 있습니다.
- 스타일 1: 하위 경로가 없는 기본 지정자
- 스타일 2: 확장자가 없는 하위 경로 포함 기본 지정자
- 스타일 3: 확장자가 있는 하위 경로 포함 기본 지정자
스타일 1: 하위 경로가 없는 기본 지정자
이전 버전과의 호환성을 위해 "main"만 제공합니다(이전 번들러 및 Node.js 12 이하).
최신 버전은 "."면 충분합니다.
package.json:
{
"main": "./dist/src/main.js",
"exports": {
".": "./dist/src/main.js"
}
}
import {someFunction} from 'my-lib';
이는 my-lib/dist/src/main.js 파일에서 해당 함수를 가져옵니다.
스타일 2 : 확장자가 없는 하위 경로 포함 기본 지정자
package.json:
{
"exports": {
"./util/errors": "./dist/src/util/errors.js"
}
}
지정자 하위 경로 'util/errors'를 모듈 파일에 매핑하고 있습니다.
다음과 같은 임포트가 가능합니다.
import {UserError} from 'my-lib/util/errors';
package.json:
{
"exports": {
"./lib/*": "./dist/src/*.js"
}
}
import {someFunction} from 'my-lib/lib/main';
import {UserError} from 'my-lib/lib/util/errors';
"./lib/*": "./dist/src/*.js"
스타일 3 : 확장자가 있는 하위 경로 포함 기본 지정자
package.json:
{
"exports": {
"./util/errors.js": "./dist/src/util/errors.js"
}
}
지정자 하위 경로 'util/errors.js'를 모듈 파일에 매핑하고 있습니다.
그러면 다음과 같은 가져오기가 가능합니다.
import {UserError} from 'my-lib/util/errors.js';
package.json:
{
"exports": {
"./*": "./dist/src/*"
}
}
import {InternalError} from 'my-package/util/errors.js';
import {InternalError} from 'my-package/dist/src/util/errors.js';
이것은 파일 시스템 글롭이 아니라 외부 모듈 지정자를 내부 지정자로 매핑하는 방법임에 유의합니다.
"./*": "./dist/src/*"
와일드카드 + 일부 파일 숨기기
다음 트릭을 사용하여 my-package/dist/src/internal/을 제외하고
my-package/dist/src/ 디렉토리의 모든 항목을 노출합니다.
"exports": {
"./*": "./dist/src/*",
"./internal/*": null
}
조건부 패키지 내보내기
Node.js vs browsers
"exports": {
".": {
"node": "./main-node.js",
"browser": "./main-browser.js",
"default": "./main-browser.js"
}
}
새 플랫폼 또는 알려지지 않은 플랫폼을 처리하기 위해 default를 사용합니다.
development vs production
"exports": {
".": {
"development": "./main-development.js",
"production": "./main-production.js",
}
}
node --conditions development app.mjs
패키지 가져오기(Package import)
패키지 가져오기(Package imports)를 사용하면 패키지가 내부적으로 사용할 수 있는 모듈 지정자에 대한 약어를 정의할 수 있습니다.
(주의! : 패키지 내보내기는 다른 패키지를 위한 약어를 정의합니다).
예를 들면 다음과 같습니다.
package.json:
{
"imports": {
"#some-pkg": {
"node": "some-pkg-node-native",
"default": "./polyfills/some-pkg-polyfill.js"
}
},
"dependencies": {
"some-pkg-node-native": "^1.2.3"
}
}
- 현재 패키지가 Node.js에서 사용되는 경우 모듈 지정자 '#some-pkg'는 패키지 some-pkg-node-native를 나타냅니다.
- 다른 환경에서 '#some-pkg'는 현재 패키지 내부의 ./polyfills/some-pkg-polyfill.js 파일을 나타냅니다.
패키지 가져오기만 외부 패키지를 참조할 수 있으며 패키지 내보내기는 외부 패키지를 참조할 수 없습니다.
- 동일한 모듈 지정자를 통해 다른 플랫폼별 구현 모듈을 참조합니다(위에서 설명한 대로).
- 같은 npm 패키지인데 일렉트론, 노드, 브라우저에 따라 다른 모듈 제공
- 현재 패키지 내부의 모듈에 대한 별칭
- 상대 지정자를 피하기 위해(깊이 중첩된 디렉토리로 인해 복잡해질 수 있음).
번들러와 함께 패키지 가져오기를 사용할 때 주의하세요.
이 기능은 비교적 신기능이며 번들러에서 지원하지 않을 수 있습니다.
node: 프로토콜 가져오기
import * as fs from 'node:fs/promises';
import * as fs from 'fs/promises';
node: 프로토콜 사용의 또 다른 이점은 가져온 모듈이 빌트인 모듈임을 즉시 확인할 수 있다는 것입니다.
다른 npm 패키지와 쉽게 구분할 수 있습니다.
노드: 프로토콜이 있는 지정자로 인해 절대적인 것으로 간주됩니다.
node_modules에서 조회되지 않는 이유입니다.
참고
https://exploringjs.com/nodejs-shell-scripting/ch_packages.html
'FrontEnd' 카테고리의 다른 글
Content security policy[콘텐츠 보안 정책]에 대해 알아보기 (0) | 2023.01.21 |
---|---|
Vue3과 서버사이드 렌더링(SSR) (0) | 2023.01.19 |
Node.JS 앱은 어떻게 종료되는가? (0) | 2023.01.15 |
Vue3로 debounce, throttle 구현하기 (0) | 2023.01.13 |
npm link를 이용하여 서드파티 npm 패키지 커스터마이징 (0) | 2023.01.12 |