Node.js 모듈 시스템 이해하기
Lukas Schneider
DevOps Engineer · Leapcell

소개
모듈 시스템은 재사용 가능한 단위로 코드를 구성하고 종속성을 효율적으로 관리할 수 있도록 함으로써 최신 JavaScript 개발의 기본입니다. 오랫동안 CommonJS(CJS)는 Node.js 서버 측 JavaScript의 사실상의 표준으로 사용되었으며, 수많은 프로젝트에 대해 강력하고 효과적인 것으로 입증되었습니다. 그러나 브라우저 및 결국 Node.js에서 ECMAScript 모듈(ESM)의 표준화와 함께 모듈 관리 환경이 크게 바뀌었습니다. 이러한 발전은 JavaScript 생태계 전반에 걸쳐 강력한 새로운 기능과 보다 통합된 모듈 시스템을 제공하지만, 개발자에게는 복잡성과 결정 지점을 도입하기도 합니다. 오늘날 유지 관리 가능하고 성능이 뛰어난 Node.js 애플리케이션을 구축하려면 CJS와 ESM 간의 미묘한 차이를 이해하고 둘을 함께 사용하는 전략을 파악하는 것이 중요합니다. 이 글에서는 이러한 차이점을 자세히 살펴보고, 두 시스템을 함께 사용하는 전략을 탐색하며, 프로젝트에 대한 실용적인 통찰력을 제공할 것입니다.
핵심 개념
자세히 알아보기 전에 Node.js 모듈 시스템과 관련된 핵심 용어를 정의해 보겠습니다.
CommonJS (CJS)
CommonJS는 주로 Node.js에서 사용되는 모듈 사양입니다. 동기식 모듈 로딩 시스템으로, 모듈이 require
될 때 런타임은 실행을 계속하기 전에 모듈이 로드되고 구문 분석될 때까지 기다립니다.
require()
: 모듈을 가져오는 데 사용되는 함수입니다. 모듈 경로를 인수로 받아 해당 모듈의exports
객체를 반환합니다.module.exports
: 모듈에서 값을 내보내는 데 사용되는 객체입니다. 기본적으로 빈 객체{}
입니다.module.exports
에 할당할 때 모듈의 전체 내보내기를 설정하는 것입니다.exports
:module.exports
에 대한 참조입니다.exports
에 속성을 추가하여 여러 값을 노출할 수 있습니다.
ES 모듈 (ESM)
ES 모듈(JavaScript 모듈 또는 ES6 모듈이라고도 함)은 ECMAScript 모듈의 공식 표준입니다. 정적이고 비동기적인 로딩 메커니즘을 특징으로 하며 브라우저와 Node.js 모두에서 작동하도록 설계되었습니다.
import
: 모듈을 가져오는 데 사용되는 문입니다. 이름이 지정된 가져오기(import { name } from './module'
) 또는 기본 가져오기(import defaultExport from './module'
)에 사용할 수 있습니다.export
: 모듈에서 값을 내보내는 데 사용되는 문입니다. 이름이 지정된 내보내기(export const name = 'value'
) 또는 기본 내보내기(export default value
)에 사용할 수 있습니다.- 정적 분석: ESM 가져오기 및 내보내기는 구문 분석 시 정적으로 분석할 수 있으므로 트리 셰이킹 및 더 나은 도구 지원이 가능합니다.
- 비동기 로딩: ESM 모듈은 비동기 로딩을 위해 설계되었으며, 이는 웹 브라우저에서 성능에 중요합니다.
package.json
type
필드
Node.js는 package.json
의 type
필드를 사용하여 패키지의 파일을 CJS 또는 ESM으로 해석해야 하는지 여부를 결정합니다.
"type": "commonjs"
: 모든.js
파일(확장자가 없는 파일 포함)은 CJS로 처리됩니다."type": "module"
: 모든.js
파일(확장자가 없는 파일 포함)은 ESM으로 처리됩니다.
type
필드와 관계없이 .mjs
파일은 항상 ESM으로 처리되고 .cjs
파일은 항상 CJS로 처리됩니다. 이를 통해 모듈 유형을 명시적으로 구분할 수 있습니다.
차이점 및 상호 운용성
CJS와 ESM의 주요 차이점은 디자인 철학과 로딩, 구문 및 범위를 처리하는 방식에서 비롯됩니다.
주요 차이점
-
구문:
- CJS: 가져오기에는
require()
를 사용하고 내보내기에는module.exports
또는exports
를 사용합니다. - ESM: 가져오기에는
import
를 사용하고 내보내기에는export
를 사용합니다.
CJS 예시:
// math.js function add(a, b) { return a + b; } module.exports = { add }; // app.js const { add } = require('./math'); console.log(add(2, 3)); // 5
ESM 예시:
// math.mjs export function add(a, b) { return a + b; } // app.mjs import { add } from './math.mjs'; console.log(add(2, 3)); // 5
- CJS: 가져오기에는
-
로딩 메커니즘:
- CJS: 동기식 로딩.
require()
는 모듈이 로드될 때까지 실행을 차단합니다. 파일 I/O가 빠른 서버 측 환경에서는 괜찮습니다. - ESM: 비동기식 로딩.
import
문은 모듈 코드가 실행되기 전에 먼저 처리됩니다. 이를 통해 병렬 로딩이 가능하며 웹 환경에 최적화되어 있습니다.
- CJS: 동기식 로딩.
-
바인딩 대 값:
- CJS: 내보내기는 값의 복사본입니다. 내보낸 값이 내보내는 모듈 내부에서 변경되면 가져오는 모듈은 업데이트된 값을 보지 못합니다.
- ESM: 내보내기는 라이브 바인딩입니다. 내보낸 값이 내보내는 모듈에서 변경되면 가져오는 모듈은 업데이트된 값을 보게 됩니다. 이는 특히 수정될 수 있는 클래스 인스턴스 또는 구성 객체와 같은 경우에 유용합니다.
ESM 라이브 바인딩 예시:
// counter.mjs export let count = 0; export function increment() { count++; } // app.mjs import { count, increment } from './counter.mjs'; console.log(count); // 0 increment(); console.log(count); // 1 (라이브 바인딩)
CJS 값 복사 예시:
// counter.js let count = 0; function increment() { count++; } module.exports = { count, increment }; // app.js const { count, increment } = require('./counter'); console.log(count); // 0 increment(); console.log(count); // 0 (복사된 값, 라이브 바인딩 아님)
-
this
컨텍스트:- CJS: CJS 모듈의 최상위 레벨에서
this
는module.exports
를 참조합니다. - ESM: ESM 모듈의 최상위 레벨에서
this
는undefined
입니다.
- CJS: CJS 모듈의 최상위 레벨에서
-
파일 확장자:
- CJS: 일반적으로
.js
(type: "module"
이 설정된 경우 제외). - ESM: 명시적으로
.mjs
를 사용하거나package.json
에type: "module"
이 설정된 경우.js
를 사용할 수 있습니다.
- CJS: 일반적으로
-
__dirname
및__filename
:- CJS: 이 전역 변수는 현재 디렉터리 이름과 파일 이름을 제공하기 위해 직접 사용할 수 있습니다.
- ESM: 이러한 변수는 직접 사용할 수 없습니다.
import.meta.url
을 사용하여 구성해야 합니다.
__dirname
및__filename
의 ESM 해당:import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); console.log(__filename); console.log(__dirname);
상호 운용성 전략
Node.js는 동일한 프로젝트 내에서 CJS와 ESM을 함께 사용하기 위한 메커니즘을 제공합니다.
-
ESM에서 CJS 가져오기: ESM 모듈은 CJS 모듈을 직접
import
할 수 있습니다. CJS 모듈의default
내보내기는 가져온 값이 됩니다. CJS 모듈의 명명된 내보내기는 직접 액세스할 수 없으며, 전체 모듈을 가져온 다음 구조 분해해야 합니다.// cjs-module.js module.exports = { greet: 'Hello CJS!', sayHi: () => 'Hi CJS!' }; // esm-app.mjs (또는 "type": "module"이 있는 .js) import cjsModule from './cjs-module.js'; // 전체 내보내기 객체를 가져옵니다. console.log(cjsModule.greet); // Hello CJS! console.log(cjsModule.sayHi()); // Hi CJS! // CJS의 명명된 가져오기는 직접 지원되지 않습니다. // import { greet } from './cjs-module.js'; // 이 경우 오류가 발생합니다.
-
CJS에서 ESM 요구 (실험적/간접적): CJS 모듈에서 ESM 모듈을 직접
require()
하는 것은 Node.js에서 기본적으로 지원되지 않습니다.require()
는 동기식이지만 ESM 로딩은 비동기식입니다.이를 달성하려면 CJS 모듈 내에서 동적
import()
를 사용해야 하며, 이는 Promise를 반환합니다. 이는 일반적으로async
함수에서 수행됩니다.// esm-module.mjs export const message = 'Hello ESM from CJS!'; // cjs-app.js async function run() { const esmModule = await import('./esm-module.mjs'); console.log(esmModule.message); // Hello ESM from CJS! // 기본 내보내기의 경우 esmModule.default가 됩니다. } run();
-
혼합 패키지 (이중 패키지 위험): CJS와 ESM을 모두 지원하는 패키지를 게시할 때 애플리케이션의 다른 부분이 패키지의 다른 인스턴스를 로드하여 예기치 않은 동작(예: 단일 패턴 클래스가 여러 인스턴스가 되는 것)을 초래할 수 있는 "이중 패키지 위험"에 직면하게 됩니다.
이를 완화하기 위해 라이브러리는 종종
package.json
의exports
필드를 사용하여 별도의 진입점을 제공합니다.{ "name": "my-package", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.mjs", "exports": { ".": { "import": "./lib/esm/index.mjs", "require": "./lib/cjs/index.js" }, "./package.json": "./package.json" }, "type": "commonjs" }
이 구성을 사용하면 다음과 같이 됩니다.
- ESM 모듈이
my-package
를 가져올 때lib/esm/index.mjs
를 로드합니다. - CJS 모듈이
my-package
를 요구할 때lib/cjs/index.js
를 로드합니다.
이렇게 하면 컨텍스트에 따라 올바른 모듈 유형이 로드됩니다.
- ESM 모듈이
애플리케이션 시나리오 및 모범 사례
- 새 프로젝트: 일반적으로 새로운 Node.js 프로젝트는 ESM을 선호해야 합니다. 이것이 표준이며 정적 분석 이점과 더 넓은 JavaScript 생태계와의 일치를 제공합니다.
package.json
에"type": "module"
을 설정하세요. - 기존 CJS 프로젝트: 대규모 CJS 프로젝트를 ESM으로 마이그레이션하는 것은 상당한 작업이 될 수 있습니다. 상호 운용성 기능을 사용한 점진적 채택이 종종 가장 실용적인 접근 방식입니다. 특히 새로운 기능에는 점진적으로 코드베이스의 일부를 ESM으로 변환하세요.
- 라이브러리 개발: 라이브러리를 구축하는 경우 가장 넓은 범위에 도달하고 이중 패키지 위험을 피하기 위해
package.json
의exports
필드를 사용하여 CJS 및 ESM을 모두 지원하는 것을 강력히 고려해야 합니다. - 도구: 일부 레거시 Node.js 도구 및 테스트 프레임워크는 CJS보다 ESM을 더 잘 지원할 수 있다는 점을 인지해야 합니다. 도구 체인의 ESM 호환성을 확인하세요.
결론
Node.js에서 CommonJS와 ES 모듈의 공존은 도전과 기회 모두를 제공합니다. CJS는 오랜 역사를 가지고 있으며 여전히 널리 사용되지만, ESM은 정적 분석 및 라이브 바인딩과 같은 상당한 이점을 제공하는 표준화된 접근 방식을 제공하는 JavaScript 모듈화의 미래를 나타냅니다. 기본 차이점을 이해하고 Node.js의 내장 상호 운용성 기능을 활용함으로써 개발자는 이 이중 모듈 환경을 효과적으로 탐색할 수 있습니다. 새로운 프로젝트에 ESM을 채택하고 기존 프로젝트에서 상호 운용성을 신중하게 관리하면 더 강력하고 유지 관리 가능하며 미래 지향적인 Node.js 애플리케이션을 만들 수 있습니다.
ESM으로의 전환은 JavaScript의 모듈 스토리를 환경 전반에 걸쳐 통일하여 코드 구성이 더욱 일관되고 강력해집니다.