모던 자바스크립트라고 부르는 현재의 Javascript ES6 표준에서는 기존 ES5와 다르게 여러가지 변화가 있었는데, 그중에 하나인 모듈 시스템에 대해서 제대로 이해하기 라는 저 나름의 정리를 위한 포스팅을 해보았습니다.
I. 자바스크립트 모듈
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules
JavaScript modules - JavaScript | MDN
이 가이드는 JavaScript 모듈 구문을 시작하는데 필요한 모든 것을 제공합니다.
developer.mozilla.org
위 MDN 문서를 참고하시는것을 추천해드립니다.
자바스크립트는 알려져있는 대로 그 시작이 간단한 효과를 웹에 추가하기 위해 시작된 언어로, 초기에는 그렇게 까지 큰 규모의 스크립트는 필요하지 않았습니다. 하지만, 이후 웹에서 점차 자바스크립트를 많이 사용하게 되었고 (웹의 폭발적 성장?) Node.js가 개발된 이후로는 웹을 점점 규모가 커져 더이상 간단한 규모가 아닌 큰 규모의 애플리케이션을 만들수 있게 되어 이제는 별도의 모듈(Module)로 분할하기 위해 고민을 하게 되었다고 합니다.
제가 아는 모던 자바스크립트(ES6)에서의 모듈 시스템은 아래와 같습니다.
- CommonJS : reqire, module.exports
- ES6 (a.k.a ES2015) : import, export
- AMD (Asynchronous Module Definition) : 이름만 들어봄..^^;
II. 모듈 사용해보기
(1) CommonJS 사용법
기본적으로 Node.js는 CommonJS를 사용하고 있습니다. 공식 문서를 참고하세요 (Node.js v22.6.0 기준)
https://nodejs.org/api/modules.html#modules-commonjs-modules
Modules: CommonJS modules | Node.js v22.6.0 Documentation
Modules: CommonJS modules# CommonJS modules are the original way to package JavaScript code for Node.js. Node.js also supports the ECMAScript modules standard used by browsers and other JavaScript runtimes. In Node.js, each file is treated as a separate mo
nodejs.org
다음은 Node.js의 공식 문서에 있는 예제를 가져왔습니다. commonJS 사용방법을 살펴봅시다.
- 가져오기 : require()
- 내보내기 : exports(=여러 property를 내보낼 때) 및 module.exports(= 여러 property를 내보내거나 단일 Object를 내보낼때)
※ 다음의 소스코드로 작성된 파일들은 같은 디렉터리에 있다고 가정합니다.
※ 모듈 기능을 사용하려면 먼저 함수나 변수를 export 해야합니다. 내보내려는 항목 앞에 export를 배치합니다.
// foo.js
const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
// circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
또 다른 예제를 살펴봅시다.
// bar.js
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`The area of mySquare is ${mySquare.area()}`);
// square.js
// Assigning to exports will not modify module, must use module.exports
module.exports = class Square {
constructor(width) {
this.width = width;
}
area() {
return this.width ** 2;
}
};
※ exports는 module.exports를 참조하는 객체로, exports에 신규 객체를 할당하면 모듈이 동작하지 않기 때문에 특별한 상황이 아니라면 module.exports를 사용하는게 좋습니다.
(2) ES6
- 가져오기 : import
- 내보내기 : export
ES6 모듈의 사용법을 예제를 통해 살펴봅시다. 아래처럼 내보내는 방식을 named exports 라고 합니다.
※ 각 항목은 (function, const 등) export 할 때 이름으로 참조되고, import 할때 이 이름을 참조하여 사용해야 합니다.
// named export
export const name = "square";
export function draw(ctx, length, x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, length, length);
return {
length: length,
x: x,
y: y,
color: color,
};
}
위와 같이 functions, var, let, const, class를 내보낼 수 있지만 최상위 항목이어야 합니다. 예를들어, 함수 안에서 export를 사용할 수 없습니다.
여러가지 항목을 내보내는 편리한 방법은 모듈(=자바스크립트의 파일을 모듈이라고 합니다) 끝에 하나의 export 문을 사용해서 내보내기 각 항목들은 콤마를 구분하여 나열하면 됩니다.
// named export
export { name, draw, reportArea, reportPerimeter };
※ 만약, export default 로 변경하려면, 모든 함수와 변수를 객체(Object)로 묶어 하나의 기본 내보내기를 만들어주어야 합니다.
// default export
const shapeModule = {
name,
draw,
reportArea,
reportPerimeter,
};
export default shapeModule;
모듈에서 일부 기능을 내보낸 후 이를 사용하려면, 우리가 사용하려는 파일로 가져와야 합니다.
// named imports
import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";
※ 만약 default import 방식으로 수정하려면 다음과 같이 작성해야 합니다.
//default import
import shapeModule from './shapeModule';
const { name, draw, reportArea, reportPerimeter } = shapeModule;
// 또는 개별적으로 사용
console.log(shapeModule.name);
shapeModule.draw();
shapeModule.reportArea();
shapeModule.reportPerimeter();
※ import문을 사용하고 가져올 목록을 쉼표로 구분하여 나열한 뒤 괄호로 묶어줍니다. 그런다음 from 뒤에 모듈 파일의 경로를 작성합니다.
※ 현재 위치를 나타내는 점(.)을 사용해, 찾고자 하는 파일의 경로를 상대 경로로 작성합니다.
※ 일부 모듈 시스템에서는 파일의 확장명을 생략할 수 있는데, (예: '/modules/square') 제대로 동작하지 않을 수 있으니 가급적이면 확장명을 기록하는게 좋겠습니다.
// main.js
let myCanvas = create("myCanvas", document.body, 480, 320);
let reportList = createReportList(myCanvas.id);
let square1 = draw(myCanvas.ctx, 50, 50, 100, "blue");
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);
만약, 모듈 시스템을 웹페이지(HTML)에서 사용하려면 <script> 요소에 type="module"을 포함하여 작성해주어야 합니다.
<script type="module" src="main.js"></script>
※ 기본적으로 모듈을 가져오는 스크립트는 최상위 모듈로 작동하며, "SyntaxError: import declarations may only appear at top level of a module" 라는 에러가 발생하는지 확인하는게 좋습니다.
※ import와 export문(statement)은 모듈 내에서만 사용가능합니다. 왜냐하면, 정규 스크립트가 아니기 때문입니다.
(3) 네이밍 충돌 피하기
동일한 이름의 여러 함수를 동일한 최상위 모듈로 가져올때는 충돌이 발생합니다. 이럴때는 as 키워드를 새 함수의 이름으로 함께 사용해, 최상위 모듈 내부의 함수들을 식별 가능한 이름으로 변경할 수 있습니다.
// inside module.js
export { function1 as newFunctionName, function2 as anotherNewFunctionName };
// inside main.js
import { newFunctionName, anotherNewFunctionName } from "./modules/module.js";
또는
// inside module.js
export { function1, function2 };
// inside main.js
import {
function1 as newFunctionName,
function2 as anotherNewFunctionName,
} from "./modules/module.js";
※ 모듈 코드는 그대로 두고 import 를 변경하는 것이 합리적이라고 볼 수 있습니다. 특히 제어 권한이 없는 써드 파티 모듈에서 import를 사용하는 경우 유용합니다.
※ 위의 방법은 다소 지저분하고 길어보일 수 있어서, 각 모듈의 기능을 모듈 객체 내부로 가져오는 방법을 사용할 수 있습니다.
import * as Module from "./modules/module.js";
Module.function1();
Module.function2();
III. 모듈 올바르게 사용하기
ES6에서는 모듈을 import, export 하여 사용합니다. 이러한 모듈 방식은 기존의 자바스크립트에서는 없던 방식으로 CommonJS가 Node.js에서는 기본 모듈로 채택되어 있지만, 실제로 많은 사용자들이 ESM(EcmaScript Module)을 사용합니다.
여전히 모던 브라우저가 아닌 일부 브라우저에서는 ES6를 지원하지 않는다는 말을 들어본적이 있는데, 이제는 대부분의 알려진 브라우저들은 ES6를 지원할 것으로 생각됩니다.
저는 주로 ES6 모듈을 사용하므로 node.js 프로젝트를 생성할때 해당 부분을 함께 설정하곤 합니다.
npm init -y; npm pkg set type="module";
// 또는
npm init es6
어떤 예제나 책에서는 여전히 require, export 키워드를 사용하는 CommonJS (내가 갖고 있는 책이..ㅠ) 코드가 있고 구글링 등으로 검색할때 보면 import, export 키워드를 사용하는 ES6 방식이 있어서 가끔 혼동이 있지만..앞으로는 잘 구분해서 사용해보고자 전체적으로 정리해보았습니다.