Node.js 개발자라면 cjs와 esm 모듈에 대해 들어본 적이 있을 것입니다. 하지만 두 가지 모듈이 있는 이유와 Node.js 애플리케이션에서 이들 모듈이 어떻게 공존하는지 잘 모를 수도 있습니다. 이 블로그 게시물에서는 Node.js의 JavaScript 모듈 역사를 간략하게 설명하므로(예제 포함) 이러한 개념을 다룰 때 더욱 자신감을 가질 수 있습니다.
초기 JavaScript에는 모든 멤버가 선언된 전역 범위만 있었습니다. 두 개의 독립적인 파일이 멤버에 대해 동일한 이름을 사용할 수 있기 때문에 코드를 공유할 때 문제가 되었습니다. 예:
greet-1.js
function greet(name) { return `Hello ${name}!`; }
greet-2.js
var greet = "...";
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Collision example</title> </head> <body> <!-- After this script, `greet` is a function --> <script src="greet-1.js"></script> <!-- After this script, `greet` is a string --> <script src="greet-2.js"></script> <script> // TypeError: "greet" is not a function greet(); </script> </body> </html>
Node.js는 CommonJS(cjs라고도 함)를 통해 JavaScript 모듈 개념을 공식적으로 도입했습니다. 개발자는 내보낼 항목(module.exports를 통해)과 가져올 항목(require()를 통해)을 결정할 수 있으므로 공유 전역 범위의 충돌 문제가 해결되었습니다. 예:
src/greet.js
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported function greet(name) { return `${GREETING_PREFIX} ${name}!`; } // `exports` is a shortcut to `module.exports` exports.greet = greet;
src/main.js
// notice the `.js` suffix is missing const { greet } = require("./greet"); // logs: Hello Alice! console.log(greet("Alice"));
Node.js 개발은 개발자가 재사용 가능한 JavaScript 코드를 게시하고 사용할 수 있게 해주는 npm 패키지 덕분에 폭발적인 인기를 얻었습니다. npm 패키지는 기본적으로 node_modules 폴더에 설치됩니다. 모든 npm 패키지에 있는 package.json 파일은 "main" 속성을 통해 Node.js의 진입점을 나타낼 수 있기 때문에 특히 중요합니다. 예:
node_modules/greeter/package.json
{ "name": "greeter", "main": "./entry-point.js" // ... }
node_modules/greeter/entry-point.js
module.exports = { greet(name) { return `Hello ${name}!`; } };
src/main.js
// notice there's no relative path (e.g. `./`) const { greet } = require("greeter"); // logs: Hello Bob! console.log(greet("Bob"));
npm 패키지는 다른 개발자의 작업을 활용할 수 있어 개발자의 생산성을 획기적으로 높였습니다. 그러나 cjs는 웹 브라우저와 호환되지 않는다는 큰 단점이 있었습니다. 이 문제를 해결하기 위해 번들러라는 개념이 탄생했습니다. browserify는 기본적으로 진입점을 탐색하고 모든 require() 코드를 웹 브라우저와 호환되는 단일 .js 파일로 "번들링"하여 작동하는 최초의 번들러였습니다. 시간이 지남에 따라 추가 기능과 차별화 요소를 갖춘 다른 번들러가 도입되었습니다. 가장 주목할만한 것은 webpack, 소포, 롤업, esbuild 및 vite입니다(연대순).
Node.js 및 cjs 모듈이 주류가 되면서 ECMAScript 사양 관리자는 모듈 개념을 포함하기로 결정했습니다. 이것이 기본 JavaScript 모듈을 ESModules 또는 esm(ECMAScript 모듈의 약어)이라고도 하는 이유입니다.
esm은 멤버 내보내기 및 가져오기를 위한 새로운 키워드와 구문을 정의하고 기본 내보내기와 같은 새로운 개념을 도입합니다. 시간이 지남에 따라 esm 모듈은 동적 import() 및 최상위 대기와 같은 새로운 기능을 얻었습니다. 예:
src/greet.js
function greet(name) { return `Hello ${name}!`; }
src/part.js
var greet = "...";
src/main.js
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Collision example</title> </head> <body> <!-- After this script, `greet` is a function --> <script src="greet-1.js"></script> <!-- After this script, `greet` is a string --> <script src="greet-2.js"></script> <script> // TypeError: "greet" is not a function greet(); </script> </body> </html>
시간이 지남에 따라 esm 구문을 cjs로 변환할 수 있는 TypeScript와 같은 언어 및 번들러 덕분에 esm은 개발자들 사이에서 널리 채택되었습니다.
수요 증가로 인해 Node.js는 버전 12.x에 esm에 대한 지원을 공식적으로 추가했습니다. cjs와의 하위 호환성은 다음과 같이 달성되었습니다.
npm 패키지 호환성과 관련하여 esm 모듈은 cjs 및 esm 진입점을 사용하여 npm 패키지를 가져올 수 있습니다. 그러나 그 반대에는 몇 가지 주의 사항이 있습니다. 다음 예를 들어보세요:
node_modules/cjs/package.json
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported function greet(name) { return `${GREETING_PREFIX} ${name}!`; } // `exports` is a shortcut to `module.exports` exports.greet = greet;
node_modules/cjs/entry.js
// notice the `.js` suffix is missing const { greet } = require("./greet"); // logs: Hello Alice! console.log(greet("Alice"));
node_modules/esm/package.json
{ "name": "greeter", "main": "./entry-point.js" // ... }
node_modules/esm/entry.js
module.exports = { greet(name) { return `Hello ${name}!`; } };
다음은 정상적으로 실행됩니다.
src/main.mjs
// notice there's no relative path (e.g. `./`) const { greet } = require("greeter"); // logs: Hello Bob! console.log(greet("Bob"));
그러나 다음은 실행되지 않습니다.
src/main.cjs
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported export function greet(name) { return `${GREETING_PREFIX} ${name}!`; }
이것이 허용되지 않는 이유는 esm 모듈은 최상위 대기를 허용하는 반면 require() 함수는 동기식이기 때문입니다. 동적 import()를 사용하도록 코드를 다시 작성할 수 있지만 Promise를 반환하므로 다음과 같은 결과가 발생합니다.
src/main.cjs
// default export: new concept export default function part(name) { return `Goodbye ${name}!`; }
이 호환성 문제를 완화하기 위해 일부 npm 패키지는 조건부 내보내기와 함께 package.json의 "내보내기" 속성을 활용하여 cjs 및 mjs 진입점을 모두 노출합니다. 예:
node_modules/esm/entry.cjs:
// notice the `.js` suffix is required import part from "./part.js"; // dynamic import: new capability // top-level await: new capability const { greet } = await import("./greet.js"); // logs: Hello Alice! console.log(greet("Alice")); // logs: Bye Bob! console.log(part("Bob"));
node_modules/esm/package.json:
{ "name": "cjs", "main": "./entry.js" }
"exports" 속성을 지원하지 않는 Node.js 버전과의 하위 호환성을 위해 "main"이 어떻게 cjs 버전을 가리키는지 확인하세요.
cjs 및 esm 모듈에 대해 (2024년 12월 기준) 알아야 할 (거의) 전부입니다. 아래에 여러분의 생각을 알려주세요!
위 내용은 Node.js: cjs, 번들러, esm의 간략한 역사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!