Node14에는 옛날 스타일의 CommonJS와 새로운 스타일의 ESM Script 두개가 공존하고 있다.
JS 모듈을 내보내거나 가져올 때 2가지 방식을 사용한다.
module.exports
로 모듈을 내보내고 require()
로 접근하는 CJS(CommonJS)export
로 모듈을 내보내고 import
로 접근하는 ESM(ES Modules)이 있다.// CJS 방법
module.exports = { ... };
const utils = require('utils');
// ESM 방법
export default = () => { ... };
import utils from 'utils';
외부 모듈에 접근할 때는 require()
을 사용한다.
const utils = require('utils');
module.exports
로 모듈을 내보낸다.
모듈을 내보내는 방식은 named exports
, default exports
두 가지가 있다.
module.exports.utils = { ... }; // named exports
module.exports = { ... }; // default exports
// calculator.js
module.exports.add = (a, b) => a + b;
module.exports.sub = (a, b) => a - b;
named exports
모듈은 2가지 방식으로 접근할 수 있다.const calculator = require('./calculator.js');
const { add } = require('./calculator.js');
console.log(calculator.add(2, 2)); // 4
console.log(add(2, 2)); // 4
default exports
모듈을 접근할 때는 원하는 이름으로 설정하여 사용할 수 있다.const add = require('./calculator2.js'); // 모듈을 add로 명명
console.log(add(2,2)); // 4
top-level await
async function
밖에서await
를 사용하게 해주는 것
트리 셰이킹
사용되지 않는 코드를 제거하여 번들 크기를 줄이는 기술
require()
는 즉시 스크립트를 실행하는 구조이다.
top-level await가 불가능하므로, 동기적으로 작동한다.
모듈이 필요한 시점에 즉시 로드되고 해당 모듈의 코드가 실행될 때까지 다음 진행이 차단된다.
브라우저 환경에서 차단은 성능 혹은 동작에 문제가 발생할 수 있다.
동기로 작동하므로 promise
를 리턴하지 않고, module.exports
에 설정된 값만을 리턴한다.
import 순서에 따라 스크립트를 실행한다.
서버 사이드 혹은 런타임에서 사용한다.
캐싱
같은 모듈이 여러번 로드되어도 한 번만 실행된다. 이는 무한 루프를 방지하고 성능을 향상시킨다.
모듈이 동기적으로 로드되므로, 비동기 로드가 필수적인 브라우저에서 사용하기 어렵다.
서버 사이드 렌더링과 같은 환경에서 유리하다.
왜냐하면, 서버 사이드에서는 모든 모듈이 로드된 후에야 코드가 실행되기 때문이다.
동적 로드를 지원하기 때문에, 트리 셰이킹이 어렵다.
ECMAScript
에서 지원하는 방식이다.ESM 방식을 사용하기 위해선 package.json
에 "type": "module"
을 설정해야 한다.
// package.json
{
"type": "module",
}
import utils from 'utils';
import { add } from 'utils';
import { add as add_func } from 'utils';
named exports
와 default exports
두 가지를 지원한다.아래는 named exports
의 예시이다.
// calculator.js
export const sum = (x, y) => x + y;
named exports
는 명명된 이름으로만 모듈을 불러올 수 있다.as
를 사용해 명명된 이름을 다른 별칭으로 수정할 수 있다.import { sum } from './calculator.js';
import { sum as sum_func } from './calculator.js';
console.log(sum(2, 4)); // 6
console.log(sum_func(2, 4)); // 6
SyntaxError
가 발생한다.// SyntaxError does not provide an export named 'default'
import calculator from "./calculator.js";
console.log(calculator.sum(2, 4)); // error
다음은 default export
의 예시이다.
CJS와 마찬가지로, 별도의 이름을 지정하지 않아도 된다.
// calculator2.js
export default (x, y) => x + y;
export default function sum(a, b) {
return a + b;
}
import add from './calculator2.js';
console.log(add(2, 4)); // 6
top-level await
async function
밖에서await
를 사용하게 해주는 것
트리 셰이킹
사용되지 않는 코드를 제거하여 번들 크기를 줄이는 기술
top-level await
를 지원하므로, module loader
가 비동기 환경에서 실행된다.
그러므로 CJS처럼 스크립트를 바로 실행하지 않고 import, export 구문을 찾아 스크립트를 파싱한다.
파싱 단계에서 import, export 에러를 감지할 수 있다.
모듈을 병렬(비동기)로 다운로드하지만, 실행은 순차적으로 한다.
import와 export를 지원하지 않는 브라우저가 있기에, ESM 사용을 위해 번들러가 필요하다.
모듈이 비동기로 로드되므로, 비동기 로드가 필수적인 브라우저 환경에서도 사용될 수 있다.
ESM은 정적으로 의존성을 분석할 수 있어서 트리 셰이킹이 용이하다.
ESM이 CommonJS의 다양한 문제점들을 해결하게 되면서 언어 표준으로 지정되었다.
따라서 ESM은 자바스크립트 언어의 일부가 되었고, Node.js 지원 환경에서만 사용 가능했던 CommonJS와는 달리 브라우저, Deno 등 다양한 런타임에서도 쉽게 사용할 수 있다.
특징 | CommonJS | ESM |
---|---|---|
로드 방식 | 동기적 | 비동기적 |
트리 셰이킹 | 어려움 | 용이 |
사용 환경 | 서버 사이드 | 브라우저 |
키워드 | require, exports | import, export |
config
파일들에서는 대부분 cjs 방식을 사용한다.
이는 아래의 결과이다.
Node.js 환경 호환성
프론트엔드의 프레임워크 혹은 라이브러리의 config 파일은 보통 Node.js 환경에서 실행된다.
Node.js는 CJS를 모듈 시스템 기본값으로 활용하기 때문에, 별다른 설정을 해줄 필요가 없어 용이한 점이 있다.
CJS와 ESM 동작의 차이
require
는 동기로 동작이 이루어지고 ESM은 import와 export 구문을 바로 실행하지 않고 비동기 환경에서 동작하도록 한다.
이는 정확한 동작 순서 예측과 어떤 곳에서 문제가 발생하는지 찾기 어렵게 만든다.
.cjs
는 CommonJS, .mjs
는 ESM로 동작한다.