Backend/Node.js

Node.js - commonJS vs ES Modules

둉이 2023. 2. 20. 20:30

 

Node.js에서 사용할 수 있는 자바스크립트 모듈 시스템 방식으로는 commonJSES Modules가 있다.

 

 

commonJS란?

// import
const 모듈명 = require('모듈 경로');

// export
module.exports = 모듈명;  // 기본적으로 내보낼 하나의 모듈 지정
module.exports = { 모듈1, 모듈2, ...  };  // 여러 모듈 한 번에 export
module.exports.모듈명 = 모듈;  // 특정 모듈 export

 

commonJS는 EMCAScript에 자바스크립트 내장 모듈 표준이 존재하지 않았을 때부터 사용되던, Node.js에서 기본적으로 제공하는 모듈 시스템 방식이다.

 

*.cjs 확장자를 사용하여 명시적으로 commonJS 모듈임을 나타낼 수 있다.

 

import할 때는 require() 함수를 이용하고, export할 때는 module.exports로 내보낼 모듈을 지정한다.

 

require() 함수는 모듈을 가져올 때, 지정한 상수명에 해당 모듈의 값의 사본을 복사하여 저장한다.

 

 

 

ES Modules란?

// import
import 모듈명 from '모듈 경로';

// export
export 모듈명;  // 특정 모듈 export
export default { 모듈1, 모듈2, ...  };  // 여러 모듈 한 번에 export
export default 모듈명;  // 기본적으로 내보낼 하나의 모듈 지정

 

자바스크립트에 모듈 시스템이 표준화되면서 도입된 방식으로, *.mjs 확장자를 사용하여 명시적으로 ES 모듈임을 나타낼 수 있다.

 

ESM은 Node.js 8.5.0버전부터 실험적으로 지원되었으며, --experimental-modules 플래그를 붙여서 실행하여 사용이 가능했다.

(13.2.0버전부터는 안정적인 지원을 제공하기 때문에 플래그가 필요 없음)

 

import, export 키워드를 사용하여 모듈을 가져오거나 내보낼 수 있다.

 

html 마크업에서 import 할 때, 스크립트 태그에 type="module" 속성을 지정해야 한다.

 

 

commonJS vs ES Modules

commonJS와 ES Modules의 특징 및 차이점은 무엇이고, 둘 중 어느 방식을 사용하는 게 더 좋을지 알아보자.

 

 

commonJS는 동기적, ES Modules는 비동기적으로 실행됨

commonJS의 require() 함수는 동기적으로 모듈을 로드한다.

 

따라서 코드상 import문의 순서대로 모듈을 가져오고, 순서대로 실행된다.

 

이러한 방식은 불러오는 모듈의 개수가 많아질수록(수십, 수백, ...) 비동기 방식보다 속도가 느리다는 단점을 갖는다.

 

반면에, ES Modules는 비동기적(병렬)으로 모듈을 로드하므로, 불러오는 모듈의 개수가 많은 경우 CommonJS 방식보다 더 효율적이다.

 

 

ES Modules는 top-level await가 가능

기본적으로 자바스크립트 파일 내에서는 top-level await이 불가능하다.

 

Top-Level await란?

비동기 함수 내부가 아닌 루트 레벨 스코프에서 await 키워드를 사용하는 것

 

따라서, await 키워드를 사용하기 위해서는 아래와 같이 즉시 실행 비동기 함수를 만든 후, 해당 함수 내에서 사용해야 한다.

 

ES Modules 시스템에서는 이러한 수고로움 없이 top-level에서의 await 키워드 사용이 가능하다.

 

 

commonJS는 어느 위치에서나 import 가능, ES Modules는 top-level 최상위에서만 가능

commonJS의 import는 require() 함수가 실행될 때 동기적으로 모듈을 가져오기 때문에 런타임 시간에 동적으로 실행된다.

 

또한 if, for문 등의 블록 스코프 내에서도 require() 함수를 이용한 모듈 가져오기가 가능하다.

const FLAG = true;

if (FLAG) {
  const markdownTable = await import('markdown-table');
  // 가능
}

 

 

ES Modules구문 분석 시간에 정적으로 실행되므로 import 구문은 호이스팅의 대상이 되어 파일의 최상단으로 끌어올려진다.

 

이러한 특징 때문에 ES Modules 시스템을 사용하면 어플리케이션 실행(런타임) 이전에도 import 구문 오류를 발견할 수 있다는 장점이 있지만, top-level 스코프가 아닌 하위 스코프(if, for문) 내에서 import 구문을 사용할 수 없다는 단점도 있다.

ESM은 top-level에서만 import 가능

 

 

ES Modules는 순환 의존성 지원

// a.js
const { delayHello } = require('./d.js');
const TIME_OFFSET = 5000;
module.exports = { TIME_OFFSET };
delayHello('mjkim');


// b.js
const { TIME_OFFSET: time } = require('./c.js');
const delayHello = (name) => {
  console.log(`hello, ${name}! / time = ${time}`);
};
module.exports = { delayHello };

 

위 예시처럼 모듈 a가 b를 참조하고, 모듈 b가 a를 참조하는 CommonJS 모듈이 있다고 가정하자.

 

commonJS 모듈 시스템은 순환 의존성을 지원하지 않기 때문에 위와 같은 코드에서 circular dependency 경고가 발생하고, time의 값으로 undefined가 반환된다.

cjs는 순환 의존 경고가 발생(값=undefined)

 

반면에, ES Modules 모듈 시스템은 순환 의존성을 지원하므로 정상적으로 값 5000이 반환된다.

// a.js
import { delayHello } from './b.js';
export const TIME_OFFSET = 5000;
delayHello('mjkim');

// b.js
import { TIME_OFFSET as time } from './a.js';
export const delayHello = (name) => {
  console.log(`hello, ${name}! / time = ${time}`);
};

esm은 정상적으로 동작(값=5000)

 

 

결론

소규모 프로젝트의 경우에는 CommonJS 방식을 사용해도 큰 이슈는 없지만, 최신 Node.js 버전을 사용하거나 프로젝트의 규모가 큰 경우에는 ES Modules 방식이 적합하다.

 

그리고 만약 npm 모듈을 만들어서 배포를 해야 한다면, CommonJS와 ES Modules 방식을 둘 다 제공하도록 모듈을 만들어 배포해야 한다.

(번들러 설정 추가 필요)

 

 

 

ES Modules 사용법

기본적으로 commonJS 모듈과 ES Modules 모듈은 동작 방식이 다르기 때문에 서로 호환되지 않는다.

 

하지만, 경우에 따라 commonJS 시스템에서 ES Modules 모듈을 import 하거나 혹은 ES Modules 시스템에서 commonJS 모듈을 import 해야 하는 경우도 있을 수 있다.

 

이러한 경우에는 어떻게 하면 좋을지 알아보자.

 

 

commonJS 시스템에서 ES Modules 사용

간혹 npm 라이브러리 중 최신 라이브러리의 경우에는 ES Modules 방식만 지원하는 라이브러리들이 있다.

 

하지만 commonJS 방식의 프로젝트에서는 ES Modules 라이브러리를 import하게 되면 다음과 같은 오류가 발생한다.

ERR_REQUIRE_ESM 에러

 

프로젝트의 모듈 시스템 방식을 변경하지 않고 해당 라이브러리를 꼭 사용해야 하는 경우에는 아래와 같이 import() 함수를 사용하여 모듈을 불러올 수 있다.

(async () => {
  const { markdownTable } = await import('markdown-table');
  const result = markdownTable([
    ['title1', 'title2'],
    ['123', '456'],
  ]);
  console.log({ result });
})();

 

 

import() 함수는 런타임에 모듈을 동적으로 가져올 수 있도록 해주는 함수이다.

 

commonJS 모듈에서는 top-level await 사용이 불가하므로 비동기 즉시 실행 함수로 구문을 감싸준 뒤, import() 함수를 사용하여 ES Modules 방식의 모듈을 불러오면 된다.

 

 

ES Modules 시스템에서 commonJS 모듈 사용

ES Modules 시스템에서도 cjs 모듈을 import하여 사용할 수 있다.

 

lodash 라이브러리의 경우, 기본적으로 commonJS 방식으로 동작하기 때문에 import 구문에 구조 분해 문법을 함께 쓰면 다음과 같은 오류가 발생한다.

import { delayHello } from './b.js';
import { throttle } from 'lodash';
// SyntaxError: Named export 'throttle' not found. The requested module 'lodash' is a CommonJS module, which may not support all module.exports as named exports.
const TIME_OFFSET = 5000;

const throttleHello = throttle(() => delayHello('mjkim'), TIME_OFFSET, {
  leading: false,
});

throttleHello();

lodash 라이브러리 import시 오류 발생

 

import 구문에서 구조 분해 문법을 사용하지 않고 변수명으로 모듈을 통째로 가져온 후에 구조 분해를 적용하면 오류가 발생하지 않는다.

import { delayHello } from './b.js';
import _ from 'lodash';  // import시 구조 분해 문법 사용 불가
const { throttle } = _;
const TIME_OFFSET = 5000;

const throttleHello = throttle(() => delayHello('mjkim'), TIME_OFFSET, {
  leading: false,
});

throttleHello();

 

 

 

참고 링크

 

CommonJS vs. ES modules in Node.js - LogRocket Blog

Learn about the differences between CommonJS and ES modules when using them in Node.js applications to organize software code.

blog.logrocket.com

 

CommonJS vs. ES Modules: Modules and Imports in NodeJS

A **module system** allows you to split up your code in different parts or to include code written by other developers. We are going to have a look into CommonJS and ES Modules.

reflectoring.io