Javascript CJS와 ESM의 사용법
JavaScript의 모듈시스템은 여러 단계를 거쳐 진화해왔다. 그 중에서는 require
키워드를 이용하여 모듈을 불러오는 CommonJS와 import
를 키워드를 이용하는 ESM 기반의 모듈시스템을 많이 사용하고 있다.(CommonJS는 node.js에서 사실상 표준, ESM은 ECMAScript에서 내놓은 표준) 두 모듈시스템은 문법이 조금씩 달라 이따금 혼란스러운 경우가 있다. 이 포스트에서 두 시스템의 문법에 대해 확실하게 짚고 넘어가보자.
JavaScript 모듈시스템에 대한 설명은 이 포스트를 참고해보자
CommonJS
CommonJS(이하 CJS)는 ES Module이 등장하기 전 서버사이드의 모듈시스템으로써 만들어졌다. 사실상 Node.js에서 표준 모듈시스템으로 사용되고 있다. CJS는 Module
객체를 이용하여 데이터를 내보낸다. 정확히는 Module 객체의 키(프로퍼티)인 exports
를 활용한다.
모듈 내보내기 - module.exports와 exports의 차이
CJS로 작성된 모듈을 보면 module.exports
와 exports
키워드가 보인다. 초기엔 두 구문은 같은 의미를 가진다. 둘 모두 Module 객체의 exports 프로퍼티의 초기 값을 가리킨다. ‘초기’라는 단어에 집중하자. 이후에 두 구문이 가르키는 객체가 달라질 수 있다는 것이다. 하나씩 차근차근 알아가보자.
Module 객체의 인스턴스가 바로 module
이고 exports 프로퍼티를 접근하기 위해 module.exports
와 같이 사용한다. 즉, module.exports
는 내보내질 객체에 직접 접근하는 것이다.
exports
키워드의 경우, module.exports
가 가지는 초기값인 빈 객체 자체이다. 초기값 자체라는 의미가 굉장히 중요하다. 객체가 바뀌게 된다면 exports
는 무용지물이 된다는 뜻이다. 예시를 통해 module.exports
와 exports
의 차이에 대해 알아보자.
exports
즉, 초기객체에 추가하는 방식이다. Javascript에서 객체에 값을 추가하는 방식대로 ‘.’ 이나 ‘[]’ 를 이용할 수 있다.
// moduleA.js
exports.a = "a";
exports['b'] = "b";
// index.js
const moduleA = require('./moduleA');
console.log(moduleA.a); // a
console.log(moduleA.b); // b
module.exports
로도 초기객체에 접근할 수 있다. 또한 객체를 교환하지 않았기 때문에exports
도 혼합해서 사용가능하다.
// moduleA.js
module.exports.a = "a";
exports.b = "b"
// index.js
const moduleA = require('./moduleA');
console.log(moduleA.a); // a
console.log(moduleA.b); // b
여기까지는 문제없이 사용가능하다. 하지만 module.exports
가 가리키는 값을 아예 바꾸어버리면 문제가 생긴다. 아래와 같이 module.exports
에 새로운 객체를 만들어 넣는다고 생각해보자.
module.exports
에 새로운 객체 할당
// moduleA.js
module.exports = {
a: "a",
b: "b"
}
// index.js
const moduleA = require('./moduleA');
console.log(moduleA.a); // a
console.log(moduleA.b); // b
module.exports
에 새로운 객체 할당 후exports
를 통한 값 추가
module.exports = {
a: "a",
b: "b"
}
exports.c = "c"
// index.js
const moduleA = require('./moduleA');
console.log(moduleA.a); // a
console.log(moduleA.b); // b
console.log(moduleA.c); // undefined
이 후 exports
로 추가한 값은 할당되지 않는다. 앞서 말했듯 객체 자체를 바꿔버렸기 때문에 exports
가 가리키는 객체는 더 이상 module.exports
가 가리키는 객체와 동일하지 않다. 이를 도식화하면 아래와 같다.
바로 아래 이미지는 객체를 바꾸기 전이다. 이때는 module.exports
와 exports
가 같은 객체를 가리키기 때문에 값 추가에서는 아무 문제 없다.
할당 객체를 바꾸고 난 후 이미지를 보면 바라보는 객체가 다르다. 모듈을 불러오는 곳에서는 Module 객체의 exports 프로퍼티를 가져온다. 즉, module.exports
에 할당된 객체를 가져오는 것이다. 위 코드의 3, 4번 예시가 이 형태를 가진다.
모듈 불러오기 - require
모듈 내보내기를 설명하서면 이미 require
키워드를 이용하여 모듈을 불러올 수 있다는 것을 확인했다. require
는 해당 Module 객체의 exports 프로퍼티에 할당된 객체를 받아온다.
// moduleA.js
module.exports = {
a: "a",
b: "b"
}
// index.js
const moduleA = require('./moduleA');
console.log(moduleA.a); // a
console.log(moduleA.b); // b
ES6의 객체 디스트럭쳐링 할당을 이용할 수도 있다.
const { a, b } = require('./moduleA');
console.log(a); // a
console.log(b); // b
ECMAScript Module
ES6부터 생겨난 표준 모듈시스템인 ES Module(이하 ESM)은 import
와 export
를 이용하여 모듈을 가져오고 내보낸다. 비동기 방식으로 작동하기 때문에 브라우저에서 사용하기 좋은 방식이다. 뿐만 아니라 CJS에서 지원하지 않는 Named Exports 기능이나 별명을 사용할 수 있는 편의 기능이 존재한다.
모듈 내보내기 - export, export default
ESM에서 모듈을 내보낼 때는 export
키워드를 사용한다. 이 때 CJS처럼 따로 이름을 정하지 않고 정해진 이름 그대로 내보낸다. 이를 Named Exports라고 한다. 다른 이름으로 내보내고 싶다면 as
키워드를 이용해서 다른 이름을 지정할 수 있다. 아래 예시를 보자.
export
뒤에 선언문을 바로 써줄수도 있고 {}
안에 넣어서 내보낼 수 있다. as
키워드를 이용해서 다른 이름으로 바꿀 수도 있다.
// moduleA.js
export const a = "a";
export function funcA() { console.log('funcA') }
const b = "b";
const c = "d";
export { b, c as d }
// index.js
import { a, funcA, b, d } from './moduleA.js'
console.log(a); // a
funcA(); // funcA
console.log(b); // b
console.log(d); // d
ESM에서는 Default Export라고 부르는 기본 내보내기 기능이 있다. export default
키워드를 사용하며 최대 1개만 내보낼 수 있다. 만약 여러 개 존재한다면 처음 정의된 것이 내보내진다.
// moduleA.js
const defaultValue = 'defaultValue';
export default defaultValue;
// index.js
import defaultValue from "./moduleA.js"
console.log(defaultValue)
모듈 불러오기 - import
ESM 모듈을 불러올땐 import
키워드를 사용한다. 여기서 모듈을 내보낼때 export
이냐 export default
로 내보냈냐에 따라 불러오는 방법도 달라진다.
export
로 내보냈다면 ES6의 객체 디스트력쳐링 방식으로 가져오며 export default
의 경우 이름 그대로 가져온다.
불러올때도 as
키워드를 이용하여 다른 이름으로 지정이 가능하다.
// moduleA.js
export const a = "a";
const b = "b";
const c = "cc";
export { b, c }
const defaultValue = 'defaultValue';
export default defaultValue;
// index.js
import defaultValue, { a, b, c as cc } from "./moduleA.js"
해당 모듈에서 내보내는 모든 값을 하나의 객체 형태로 로드할 수 있는 방법도 있다. * as 이름
형태로 불러올 수 있다. 이 경우 export default
로 내보내는 값은 이름.default
로 받아올 수 있다.
// index.js
import * as a from "./moduleA.js"
console.log(a.default) // defaultValue
참고
댓글남기기