Javascript CJS와 ESM의 사용법

JavaScript의 모듈시스템은 여러 단계를 거쳐 진화해왔다. 그 중에서는 require 키워드를 이용하여 모듈을 불러오는 CommonJSimport를 키워드를 이용하는 ESM 기반의 모듈시스템을 많이 사용하고 있다.(CommonJS는 node.js에서 사실상 표준, ESM은 ECMAScript에서 내놓은 표준) 두 모듈시스템은 문법이 조금씩 달라 이따금 혼란스러운 경우가 있다. 이 포스트에서 두 시스템의 문법에 대해 확실하게 짚고 넘어가보자.

JavaScript 모듈시스템에 대한 설명은 이 포스트를 참고해보자


CommonJS

CommonJS(이하 CJS)는 ES Module이 등장하기 전 서버사이드의 모듈시스템으로써 만들어졌다. 사실상 Node.js에서 표준 모듈시스템으로 사용되고 있다. CJS는 Module 객체를 이용하여 데이터를 내보낸다. 정확히는 Module 객체의 키(프로퍼티)인 exports를 활용한다.

모듈 내보내기 - module.exports와 exports의 차이

CJS로 작성된 모듈을 보면 module.exportsexports 키워드가 보인다. 초기엔 두 구문은 같은 의미를 가진다. 둘 모두 Module 객체의 exports 프로퍼티의 초기 값을 가리킨다. ‘초기’라는 단어에 집중하자. 이후에 두 구문이 가르키는 객체가 달라질 수 있다는 것이다. 하나씩 차근차근 알아가보자.

Module 객체의 인스턴스가 바로 module이고 exports 프로퍼티를 접근하기 위해 module.exports와 같이 사용한다. 즉, module.exports는 내보내질 객체에 직접 접근하는 것이다.
exports 키워드의 경우, module.exports가 가지는 초기값인 빈 객체 자체이다. 초기값 자체라는 의미가 굉장히 중요하다. 객체가 바뀌게 된다면 exports는 무용지물이 된다는 뜻이다. 예시를 통해 module.exportsexports의 차이에 대해 알아보자.

  • 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.exportsexports가 같은 객체를 가리키기 때문에 값 추가에서는 아무 문제 없다.

cjs_module
할당된 객체를 바꾸기 이전

할당 객체를 바꾸고 난 후 이미지를 보면 바라보는 객체가 다르다. 모듈을 불러오는 곳에서는 Module 객체의 exports 프로퍼티를 가져온다. 즉, module.exports에 할당된 객체를 가져오는 것이다. 위 코드의 3, 4번 예시가 이 형태를 가진다.

cjs_module_exports
할당된 객체를 바꾼 후


모듈 불러오기 - 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)은 importexport 를 이용하여 모듈을 가져오고 내보낸다. 비동기 방식으로 작동하기 때문에 브라우저에서 사용하기 좋은 방식이다. 뿐만 아니라 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


참고

카테고리:

업데이트:

댓글남기기