JavaScript 의 모듈 해결은 복잡한 주제입니다.
생태계는 현재 CommonJS 모듈에서 네이티브 ES 모듈로의 수년에 걸친 전환 과정에 있습니다. TypeScript 는 ESM 과 호환되지 않는 임포트 확장에 대한 자체 규칙 세트를 강제합니다. 다양한 빌드 도구는 서로 호환되지 않는 별도의 메커니즘을 통해 경로 재매핑을 지원합니다.
Bun 은 일관되고 예측 가능한 모듈 해결 시스템을 제공하는 것을 목표로 합니다. 불행히도 여전히 상당히 복잡합니다.
구문
다음 파일을 고려해 보세요.
import { hello } from "./hello";
hello();export function hello() {
console.log("Hello world!");
}index.ts 를 실행하면 "Hello world!" 가 출력됩니다.
bun index.ts
Hello world!이 경우 ./hello 인 상대 경로를 확장자 없이 임포트하고 있습니다. 확장자가 있는 임포트는 선택이지만 지원됩니다. 이 임포트를 해결하기 위해 Bun 은 다음 파일을 순서대로 확인합니다.
./hello.tsx./hello.jsx./hello.ts./hello.mjs./hello.js./hello.cjs./hello.json./hello/index.tsx./hello/index.jsx./hello/index.ts./hello/index.mjs./hello/index.js./hello/index.cjs./hello/index.json
임포트 경로에는 선택적으로 확장자를 포함할 수 있습니다. 확장자가 있으면 Bun 은 해당 정확한 확장자를 가진 파일만 확인합니다.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // 이것도 작동합니다from "*.js{x}" 를 임포트하면 Bun 은 TypeScript 의 ES 모듈 지원 과 호환되도록 일치하는 *.ts{x} 파일도 추가로 확인합니다.
import { hello } from "./hello";
import { hello } from "./hello.ts"; // 이것도 작동합니다
import { hello } from "./hello.js"; // 이것도 작동합니다Bun 은 ES 모듈 (import/export 구문) 과 CommonJS 모듈 (require()/module.exports) 을 모두 지원합니다. 다음 CommonJS 버전도 Bun 에서 작동합니다.
const { hello } = require("./hello");
hello();function hello() {
console.log("Hello world!");
}
exports.hello = hello;그렇다고 해도 새 프로젝트에서는 CommonJS 사용이 권장되지 않습니다.
모듈 시스템
Bun 은 CommonJS 와 ES 모듈을 기본적으로 지원합니다. ES 모듈은 새 프로젝트에 권장되는 모듈 형식이지만 CommonJS 모듈은 여전히 Node.js 생태계에서 널리 사용됩니다.
Bun 의 JavaScript 런타임에서 require 는 ES 모듈과 CommonJS 모듈 모두에서 사용할 수 있습니다. 대상 모듈이 ES 모듈이면 require 는 모듈 네임스페이스 객체 (import * as 와 동일) 를 반환합니다. 대상 모듈이 CommonJS 모듈이면 module.exports 객체를 반환합니다 (Node.js 에서와 동일).
| 모듈 타입 | require() | import * as |
|---|---|---|
| ES 모듈 | 모듈 네임스페이스 | 모듈 네임스페이스 |
| CommonJS | module.exports | default 는 module.exports, module.exports 의 키는 named exports |
require() 사용
.ts 또는 .mjs 파일을 포함한 모든 파일이나 패키지를 require() 할 수 있습니다.
const { foo } = require("./foo"); // 확장자는 선택 사항
const { bar } = require("./bar.mjs");
const { baz } = require("./baz.tsx");CommonJS 모듈이란?">
2016 년 ECMAScript 는 ES 모듈 지원을 추가했습니다. ES 모듈은 JavaScript 모듈의 표준입니다. 그러나 수백만 개의 npm 패키지는 여전히 CommonJS 모듈을 사용합니다.
CommonJS 모듈은 module.exports 를 사용하여 값을 내보내는 모듈입니다. 일반적으로 require 는 CommonJS 모듈을 임포트하는 데 사용됩니다.
const stuff = require("./stuff");
module.exports = { stuff };CommonJS 와 ES 모듈의 가장 큰 차이점은 CommonJS 모듈은 동기식이고 ES 모듈은 비동기식이라는 것입니다. 다른 차이점도 있습니다.
- ES 모듈은 최상위
await를 지원하지만 CommonJS 모듈은 지원하지 않습니다. - ES 모듈은 항상 strict mode 에 있지만 CommonJS 모듈은 그렇지 않습니다.
- 브라우저는 CommonJS 모듈을 기본적으로 지원하지 않지만
<script type="module">을 통해 ES 모듈을 기본적으로 지원합니다. - CommonJS 모듈은 정적으로 분석할 수 없지만 ES 모듈은 정적 임포트와 내보내기만 허용합니다.
CommonJS 모듈: JavaScript 에서 사용되는 모듈 시스템의 한 유형입니다. CommonJS 모듈의 주요 기능 중 하나는 동기적으로 로드되고 실행된다는 것입니다. 즉, CommonJS 모듈을 임포트하면 해당 모듈의 코드가 즉시 실행되고 프로그램이 다음 작업으로 진행하기 전에 완료될 때까지 기다립니다. 책을 처음부터 끝까지 건너뛰지 않고 읽는 것과 유사합니다.
ES 모듈 (ESM): JavaScript 에 도입된 또 다른 유형의 모듈 시스템입니다. CommonJS 와는 약간 다른 동작을 합니다. ESM 에서 정적 임포트 (import 문을 사용한 임포트) 는 CommonJS 와 마찬가지로 동기식입니다. 즉, 일반 import 문을 사용하여 ESM 을 임포트하면 해당 모듈의 코드가 즉시 실행되고 프로그램은 단계별로 진행됩니다. 책을 한 페이지씩 읽는 것과 같습니다.
동적 임포트: 이제 혼란스러울 수 있는 부분이 나옵니다. ES 모듈은 import() 함수를 통해 모듈을 즉시 임포트하는 것도 지원합니다. 이는 "동적 임포트"라고 하며 비동기식이므로 메인 프로그램 실행을 차단하지 않습니다. 대신 프로그램이 계속 실행되는 동안 백그라운드에서 모듈을 가져와서 로드합니다. 모듈이 준비되면 사용할 수 있습니다. 이는 책을 읽는 동안 책을 멈추지 않고 추가 정보를 찾는 것과 같습니다.
요약:
- CommonJS 모듈과 정적 ES 모듈 (
import문) 은 책을 처음부터 끝까지 읽는 것과 유사한 동기식 방식으로 작동합니다. - ES 모듈은
import()함수를 사용하여 모듈을 비동기적으로 임포트하는 옵션도 제공합니다. 이는 책을 읽는 중간에 추가 정보를 찾는 것과 같습니다.
import 사용
.cjs 파일을 포함한 모든 파일이나 패키지를 import 할 수 있습니다.
import { foo } from "./foo"; // 확장자는 선택 사항
import bar from "./bar.ts";
import { stuff } from "./my-commonjs.cjs";import 와 require() 함께 사용
Bun 에서는 같은 파일에서 import 나 require 를 사용할 수 있습니다. 둘 다 항상 작동합니다.
import { stuff } from "./my-commonjs.cjs";
import Stuff from "./my-commonjs.cjs";
const myStuff = require("./my-commonjs.cjs");최상위 await
이 규칙의 유일한 예외는 최상위 await 입니다. require() 함수는 본질적으로 동기식이므로 최상위 await 를 사용하는 파일을 require() 할 수 없습니다.
다행히 매우 적은 라이브러리만 최상위 await 를 사용하므로 이는 거의 문제가 되지 않습니다. 하지만 애플리케이션 코드에서 최상위 await 를 사용하는 경우 해당 파일이 애플리케이션의 다른 곳에서 require() 되지 않는지 확인하세요. 대신 import 또는 동적 import() 를 사용해야 합니다.
패키지 임포트
Bun 은 Node.js 모듈 해결 알고리즘을 구현하므로 베어 지정자를 사용하여 node_modules 에서 패키지를 임포트할 수 있습니다.
import { stuff } from "foo";이 알고리즘의 전체 사양은 공식적으로 Node.js 문서 에 문서화되어 있습니다. 여기서는 다시 설명하지 않겠습니다. 간단히 말해, from "foo" 를 임포트하면 Bun 은 foo 패키지를 포함하는 node_modules 디렉토리를 찾기 위해 파일 시스템을 위로 스캔합니다.
NODE_PATH
Bun 은 추가 모듈 해결 디렉토리를 위해 NODE_PATH 를 지원합니다.
NODE_PATH=./packages bun run src/index.js// packages/foo/index.js
export const hello = "world";
// src/index.js
import { hello } from "foo";여러 경로는 플랫폼의 구분 기호 (Unix 는 :, Windows 는 ;) 를 사용합니다.
NODE_PATH=./packages:./lib bun run src/index.js # Unix/macOS
NODE_PATH=./packages;./lib bun run src/index.js # Windowsfoo 패키지를 찾으면 Bun 은 package.json 을 읽어 패키지를 어떻게 임포트해야 하는지 결정합니다. 패키지의 진입점을 결정하기 위해 Bun 은 먼저 exports 필드를 읽고 다음 조건을 확인합니다.
{
"name": "foo",
"exports": {
"bun": "./index.js",
"node": "./index.js",
"require": "./index.js", // 임포터가 CommonJS 인 경우
"import": "./index.mjs", // 임포터가 ES 모듈인 경우
"default": "./index.js"
}
}package.json 에서 이 조건 중 첫 번째 로 발생하는 것이 패키지의 진입점을 결정하는 데 사용됩니다.
Bun 은 하위 경로 "exports" 와 "imports" 를 존중합니다.
{
"name": "foo",
"exports": {
".": "./index.js"
}
}하위 경로 임포트와 조건부 임포트는 함께 작동합니다.
{
"name": "foo",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}Node.js 에서와 마찬가지로 "exports" 맵에서 하위 경로를 지정하면 다른 하위 경로를 임포트할 수 없습니다. 명시적으로 내보낸 파일만 임포트할 수 있습니다. 위의 package.json 이 주어지면:
import stuff from "foo"; // 이것은 작동합니다
import stuff from "foo/index.mjs"; // 이것은 작동하지 않습니다NOTE
**TypeScript 제공** — Bun 은 특별한 `"bun"` 내보내기 조건을 지원합니다. 라이브러리가 TypeScript 로 작성된 경우 (트랜스파일되지 않은!) TypeScript 파일을 직접 `npm` 에 게시할 수 있습니다. `"bun"` 조건에서 패키지의 `*.ts` 진입점을 지정하면 Bun 은 TypeScript 소스 파일을 직접 임포트하고 실행합니다.exports 가 정의되지 않으면 Bun 은 "module"(ESM 임포트만) 다음 "main" 으로 폴백합니다.
{
"name": "foo",
"module": "./index.js",
"main": "./index.js"
}사용자 정의 조건
--conditions 플래그를 사용하면 package.json "exports" 에서 패키지를 해결할 때 사용할 조건 목록을 지정할 수 있습니다.
이 플래그는 bun build 와 Bun 런타임 모두에서 지원됩니다.
# bun build 와 함께 사용:
bun build --conditions="react-server" --target=bun ./app/foo/route.js
# Bun 런타임과 함께 사용:
bun --conditions="react-server" ./app/foo/route.jsBun.build 를 사용하여 프로그래밍 방식으로 conditions 를 사용할 수도 있습니다.
await Bun.build({
conditions: ["react-server"],
target: "bun",
entryPoints: ["./app/foo/route.js"],
});경로 재매핑
Bun 은 편집기와 잘 작동하는 tsconfig.json 의 TypeScript compilerOptions.paths 를 통해 임포트 경로 재매핑을 지원합니다. TypeScript 사용자가 아니라면 프로젝트 루트에 jsconfig.json 을 사용하여 동일한 동작을 달성할 수 있습니다.
{
"compilerOptions": {
"paths": {
"config": ["./config.ts"], // 지정자를 파일에 매핑
"components/*": ["components/*"] // 와일드카드 매칭
}
}
}Bun 은 package.json 에서 Node.js 스타일의 하위 경로 임포트 도 지원합니다. 여기서 매핑된 경로는 # 으로 시작해야 합니다. 이 접근 방식은 편집기와 잘 작동하지 않지만 두 옵션을 함께 사용할 수 있습니다.
{
"imports": {
"#config": "./config.ts", // 지정자를 파일에 매핑
"#components/*": "./components/*" // 와일드카드 매칭
}
}Bun 에서 CommonJS 상호 운용성의 저수준 세부 정보">
Bun 의 JavaScript 런타임은 CommonJS 를 기본적으로 지원합니다. Bun 의 JavaScript 트랜스파일러가 module.exports 사용을 감지하면 파일을 CommonJS 로 처리합니다. 그런 다음 모듈 로더는 트랜스파일된 모듈을 다음과 같은 모양의 함수로 래핑합니다.
(function (module, exports, require) {
// 트랜스파일된 모듈
})(module, exports, require);module, exports, require 는 Node.js 의 module, exports, require 와 매우 유사합니다. 이들은 C++ 에서 with scope 를 통해 할당됩니다. 내부 Map 은 exports 객체를 저장하여 모듈이 완전히 로드되기 전에 순환 require 호출을 처리합니다.
CommonJS 모듈이 성공적으로 평가되면 Synthetic Module Record 가 생성되며 default ES 모듈 내보내기는 module.exports 로 설정되고 module.exports 객체의 키는 named exports 로 재내보내집니다 (module.exports 객체가 객체인 경우).
Bun 의 번들러를 사용할 때는 다르게 작동합니다. 번들러는 CommonJS 모듈을 module.exports 객체를 반환하는 require_${moduleName} 함수로 래핑합니다.
import.meta
import.meta 객체는 모듈이 자신에 대한 정보를 액세스하는 방법입니다. 이는 JavaScript 언어의 일부이지만 그 내용은 표준화되어 있지 않습니다. 각 "호스트"(브라우저, 런타임 등) 는 import.meta 객체에서 원하는 속성을 자유롭게 구현할 수 있습니다.
Bun 은 다음 속성을 구현합니다.
import.meta.dir; // => "/path/to/project"
import.meta.file; // => "file.ts"
import.meta.path; // => "/path/to/project/file.ts"
import.meta.url; // => "file:///path/to/project/file.ts"
import.meta.main; // 이 파일이 `bun run` 에 의해 직접 실행되면 `true`
// 그렇지 않으면 `false`
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"| 속성 | 설명 |
|---|---|
import.meta.dir | 현재 파일이 포함된 디렉토리의 절대 경로 (예: /path/to/project). CommonJS 모듈 (및 Node.js) 의 __dirname 과 동일 |
import.meta.dirname | Node.js 호환성을 위한 import.meta.dir 의 별칭 |
import.meta.env | process.env 의 별칭 |
import.meta.file | 현재 파일의 이름 (예: index.tsx) |
import.meta.path | 현재 파일의 절대 경로 (예: /path/to/project/index.ts). CommonJS 모듈 (및 Node.js) 의 __filename 과 동일 |
import.meta.filename | Node.js 호환성을 위한 import.meta.path 의 별칭 |
import.meta.main | 현재 파일이 현재 bun 프로세스의 진입점인지 여부를 나타냅니다. 파일이 bun run 에 의해 직접 실행되고 있습니까, 아니면 다른 스크립트에서 임포트되고 있습니까? |
import.meta.resolve | 모듈 지정자 (예: "zod" 또는 "./file.tsx") 를 url 로 해결합니다. 브라우저의 import.meta.resolve 와 동일합니다. 예: import.meta.resolve("zod") 는 "file:///path/to/project/node_modules/zod/index.ts" 를 반환합니다 |
import.meta.url | 현재 파일의 string url (예: file:///path/to/project/index.ts). 브라우저의 import.meta.url 과 동일 |