자바스크립트가 만들어진지 얼마 되지 않았을 때에는 스크립트의 크기도 작고 기능도 단순했기 때문에 모듈 시스템이 존재하지 않았다.
그런데 node.js
와 같은 자바스크립트 런타임 환경이 등장하는 등 자바스크립트가 활용되는 곳이 점차 많아지고 기능도 복잡해지면서 코드를 여러 개의 파일로 분할해야 할 필요성이 대두되었다.
노드 환경에서는 CommonJS(CJS)
라는 것을 기본 모듈 시스템으로 채택해서 사용하고 있었고, 이후에 ES6
버전에 들어서는 브라우저에서 사용할 수 있는 모듈 시스템인 ECMAScript Module(ESM)
이 등장하게 되었다.
이번 글에서는 CommonJS
와 ESM
의 사용 방법이나 차이점에 대해서 정리해 보고자 한다.
CommonJS
node.js
에서 기본적으로 사용되는 모듈 시스템이다.
require()
함수를 통해서 외부의 모듈을 가져오고, module
객체를 통해서 모듈을 내보낼 수 있다.
내보내기
const add = (a, b) => a + b;
const minus = (a, b) => a - b;
const calculator = {
add,
minus,
};
module.add = add;
module.minus = minus;
module.exports = calculator;
가져오기
CommonJS
에서는 require()
함수에 정확한 확장자 명을 주지 않아도 정상적으로 작동하는데, 이는 require()
함수가 자체적으로 순회하면서 파일을 찾아주기 때문이다. 다만 성능에는 좋지 않은 영향을 준다고 한다.
const calculator = require('./calculator');
const { add, minus } = require('./calculator');
console.log(`1 + 2 = ${calculator.add(1, 2)}`);
console.log(`5 + 10 = ${add(5, 10)}`);
console.log(`15 - 3 = ${minus(15, 3)}`);
ECMAScript Module
ES6
에서 등장한 표준 모듈 시스템으로, 브라우저, node.js
, Deno
등 다양한 런타임 환경에서 사용할 수 있다.
import
키워드를 통해서 외부의 모듈을 가져오고, export
키워드를 통해서 모듈을 내보낼 수 있다.
<script>
태그로 가져올 때는 type="module"
속성을 줘야 한다.
내보내기
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b;
const calculator = {
add,
minus,
};
export default calculator;
가져오기
ESM
모듈을 가져올 때 주의해야할 점이 있는데, 반드시 확장자 명을 붙혀야한다는 점이다!!
VSCode
의 자동완성 기능을 사용하다 보면 간혹 확장자 명을 붙여주지 않아서 모듈을 정상적으로 가져올 수 없던 경우가 있었는데 이 점을 유의해야 할 것 같다.
js 파일에서
import calculator, { add, minus } from './calculator.js';
console.log(`1 + 2 = ${calculator.add(1, 2)}`);
console.log(`5 + 10 = ${add(5, 10)}`);
console.log(`15 - 3 = ${minus(15, 3)}`);
html 파일에서
<html>
<head>
<script type="module">
import calculator, { add, minus } from './calculator.js';
console.log(`1 + 2 = ${calculator.add(1, 2)}`);
console.log(`5 + 10 = ${add(5, 10)}`);
console.log(`15 - 3 = ${minus(15, 3)}`);
</script>
</head>
</html>
특징
- 모듈은 HTTP 또는 HTTPS 프로토콜을 통해서만 동작함
로컬에서 단순히 html 파일을 열어서file://
프로토콜을 사용하면 모듈이 동작하지 않는다.
모듈을 사용하기 위해서는 반드시 HTTP 나 HTTPS 프로토콜을 통해야 하며,Live Server
나Vite
같이 런타임 환경을 제공해주는 도구가 필요하다. - 모듈 스코프
모듈은 자신만의 스코프가 존재한다. 따라서 모듈 내에서var
키워드로 자원을 선언하더라도 외부에 영향을 주지 않는다. - this 값
모듈 최상위 레벨에서의this
의 값은undefined
이다.
일반 스크립트의this
는 전역 객체였던 점과 대조되는 부분이다. - 단 한 번만 평가됨
동일한 모듈이 여러 곳에서 여러 번import
해서 사용되더라도 모듈은 최초 호출시 단 한 번만 실행된다. - 지연 실행
모듈 스크립트는 항상 지연 실행되기 때문에defer
속성과 동일하게 동작한다.
따라서 HTML 문서가 완전히 준비된 이후에야 모듈 스크립트가 실행된다. - strict 모드로 동작함
가짜 import
import { Component } from './Component';
React
로 개발했었을때 위와 같은 import
문을 자주 사용했었다. 생긴 것은 ESM
처럼 생겼지만 사실 확장자 명이 정확하게 입력되지 않았는데도 컴포넌트를 정상적으로 불러와졌기에 ESM
은 아니다.
TypeScript
나 Babel
같은 트랜스파일러를 사용하면 ESM
처럼 생긴 import
를 자체적으로 require()
함수를 호출하는 구문으로 변경하기 때문에 위와 같은 코드가 정상 작동했던 것이다. (22:03)
Node.js
Node.js
에서의 모듈 시스템은 기본적으로 commonJS
를 사용한다.
다만, package.json
에 { "type": "module" }
옵션을 추가하면 ESM
을 사용하도록 설정할 수 있다.
또 설정과는 별개로 .cjs
확장자에 대해서는 commonJS
로 동작하고, .mjs
확장자에 대해서는 ESM
으로 동작한다.
참고 자료
모던 자바스크립트 Deep Dive 48장 모듈
require vs import 문법 비교 (CommonJS vs ES6) (Inpa)
코어 자바스크립트 모듈
FECONF 2022 [B4] 내 import 문이 그렇게 이상했나요?