● 모듈의 일반적인 의미
모듈이란 무엇인가?
우리가 어떤 문제를 해결할 때, 막상 문제에 도전한다면 해결하기 어려울 것이다. 하지만 문제를 쪼개어 하나씩 해결한다면 조금 더 쉬울 것이다. 소프트웨어 개발에서도 마찬가지이다. 실제로 현업에서도 가장 먼저하는 것이 실제 개발할 수 있는 작은 단위로 쪼개는 것이다. 따라서 작은 단위로 나누는 것을 '모듈화' 라고 한다.
모듈화란 코드의 가독성과 효율성을 위해 단위 별로 기능을 쪼개어 개별 파일로 만들어 관리하는 것을 말한다.
모듈을 사용했을 때의 장점?
- 자주 사용되는 코드를 별도의 파일로 만들어서 필요할 때마다 재활용할 수 있다.
- 코드를 개선하면 이를 사용하고 있는 모든 애플리케이션의 동작이 개선된다.
- 코드 수정 시에 필요한 로직을 빠르게 찾을 수 있다.
- 필요한 로직만을 로드해서 메모리의 낭비를 줄일 수 있다.
- 한번 다운로드된 모듈은 웹브라우저에 의해서 저장되기 때문에 동일한 로직을 로드 할 때 시간과 네트워크 트래픽을 절약 할 수 있다. (브라우저에서만 해당)
● 자바스크립트의 모듈
모듈이 성립하려면 모듈은 자신만의 파일 스코프를 가질 수 있어야 한다. 하지만 자바스크립트는 개별적인 파일 스코프가 보장되지 않았었다. 다시 말해, 자바스크립트는 모듈이 성립하기 위해 필요한 파일 스코프 import와 export를 지원하지 않았다.
자바스크립트 파일을 여러개 만들어 HTML 문서에 script 태그로 로드해도 분리된 자바스크립트 파일은 결국 하나의 파일로 동작된다. 따라서 각각 분리된 자바스크립트 파일에 있는 변수가 중복되는 등의 문제가 발생한다.
// module_1.js
var a = 1;
console.log(a) // 1
// module_2.js
console.log(a) // 1
var a = 20;
console.log(a) // 20
자바스크립트를 브라우저(클라이언트 사이드) 뿐만 아니라 서버에서도 범용적으로 사용하기 위해 모듈 시스템은 반드시 해결해야하는 문제가 되었다. 이런 상황에서 등장한 것이 Common JS, AMD다.
● Common JS
CommonJS(http://www.commonjs.org/) 는 JavaScript를 브라우저에서뿐만 아니라, 서버사이드 애플리케이션이나 데스크톱 애플리케이션에서도 사용하려고 조직한 자발적 워킹 그룹이다. CommonJS의 'Common'은 JavaScript를 브라우저에서만 사용하는 언어가 아닌 일반적인 범용 언어로 사용할 수 있도록 하겠다는 의지를 나타내고 있는 것이라고 이해할 수 있다.
- 스코프 : 모든 모듈은 자신만의 독립적인 실행 영역이 있어야 한다.
- 정의 : 모듈 정의는 exports 객체를 이용한다.
- 사용 : 모듈 사용은 require 함수를 이용한다.
// module_1.js
module.exports = {
sum : function(a, b){ return a + b}
}
// module_2.js
const sum = require('./module_1.js').sum;
console.log(sum(1, 3)) // 4
module.exports = {
multiply : function(a,b){return a*b}
}
// module_3.js
const multiply = require('./module_2.js').multiply;
console.log(multiply(3,6)) // 18
- 장점
- 단순하고 직관적이다.
- 파일을 비동기적으로 로딩 가능한 서버환경에 적합(Node.js)하다.
- 단점
- script 파일 로딩시 blocking이 발생하는 브라우저 환경에서는 성능저하 발생.
- 이를 극복하기 위해 script 태그 동적 삽입 방식을 활용하나, 깔끔한 해결책이 될 수는 없다.
- 노드 JS에서는 파일 비동기 접근이 되어 그 순간에 바로 변수에 할당한다. 하지만 브라우저 환경에서는 그렇지 않다. 요청은 하지만 동기적으로 이루어지기에 해당 변수에는 undefined가 담기고 넘어간다. 이후 다운로드가 완료되면, 그때 추가 동작이 없기에 변수는 영원히 undefined이다.
하지만 이런 방식은 브라우저에서 결정적인 단점이 있다. 필요한 모듈을 모두 내려받을 때까지 아무것도 할 수 없게 되는 것이다. 이 단점을 극복하려는 방식이 Common JS에서 논의 됐었지만, 결국 동적으로 <script> 태그를 삽입하는 방법으로 가닥을 잡는다.
비동기 모듈 로드 문제
JavaScript가 브라우저에서 동작할 때는 서버 사이드 JavaScript와 달리 파일 단위의 스코프가 없다. 따라서 한 자바스크립트 파일에 있는 변수가 다른 자바스크립트의 변수를 모두 덮어쓰게 되는 전역변수 문제도 발생한다.
● AMD (Asynchronous Module Definition. RequireJS는 AMD 명세의 구현체. )
- CommonJS가 브라우저환경에서의 비동기 모듈 로드에 대한 관심이 저조한 것에 대한 반감으로 분리되어 나온 집단.
- 않의 왜 브라우저를 고려를 않해주나요?
// math.js 배열안에 필요한 의존성이 들어온다.
define([], function () {
function sum (a, b) { return a + b; }
return {
sum: sum
}
});
// sub1.js math는 math.js 에서 return한 sum 객체가 된다.
define(['math'], function (math) {
function plusTwo (a) {
return math.sum(a, 2);
}
return {
plusTwo: plusTwo
}
});
// main.js
require(['sub1'], function (sub) {
console.log(sub.plusTwo(10)); // 12
});
- 현재는 클라이언트 사이드 자바스크립트에서 동작하는 ES6 모듈을 사용가능하다. (export, import)
- 장점
- browser 환경에서 비동기 네트워크 로딩 처리가 가능.
- Lazy Loading 처리에도 용이하다.
- 단점
- 의존성 주입 개념을 이해하기에 난이도가 다소 높음.
- 코드가 복잡하며, 주입소스의 순서를 지켜야 하므로 오류 발생 가능성이 높음
- 장점
● 결론
- 현재는 CommonJS 모듈화를 많이 사용한다. npm으로 설치하고 webpack으로 브라우저의 처리 방식 또한 수정할 수 있기 때문이다. ES6 이전의 코드를 보면 CommonJS로 모듈화가 되어 있다.
- 지금은 ES6의 export와 import를 많이 사용한다.
● Common JS 모듈
- Node.js에서 많이 사용하는 방법이다. export와 require 방식이다.
- <script> 태그를 사용하는 브라우저 환경에서는 물론이고, NodeJS에서도 CommonJS를 기본 모듈 시스템으로 채택하고 있기 때문에, Babel과 같은 ES6 코드를 변환(transpile)해주는 도구를 사용할 수 없는 상황에서는 좋든 싫든 require 키워드를 사용해야 한다.
- 하나의 파일에서 여러 개의 객체를 내보낼 경우, exports 변수의 속성으로 할당한다.
- 하나의 파일에서 하나의 객체를 내보낼 경우, module.exports 변수 자체에 할당한다.
// Math.js
const plus = (num1, num2) => num1 + num2;
const minus = (num1, num2) => num1 - num2;
const multi = (num1, num2) => num1 * num2;
const division = (num1, num2) => num1 / num2;
//모듈 내보내기
exports.plus = plus;
exports.minus = minus;
exports.multi = multi;
exports.division = division;
console.log(module);
/*
Module {
id: '.',
path: 'C:\\Users\\js953\\Desktop\\Github\\codestate\\deepJs',
exports: {
plus: [Function: plus],
minus: [Function: minus],
multi: [Function: multi],
division: [Function: division]
},
filename: 'C:\\Users\\js953\\Desktop\\Github\\codestate\\deepJs\\Math.js',
loaded: false,
children: [],
paths: [
'C:\\Users\\js953\\Desktop\\Github\\codestate\\deepJs\\node_modules',
'C:\\Users\\js953\\Desktop\\Github\\codestate\\node_modules',
'C:\\Users\\js953\\Desktop\\Github\\node_modules',
'C:\\Users\\js953\\Desktop\\node_modules',
'C:\\Users\\js953\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]
}
*/
// index.js
//math 변수에 Math.js 파일에서 exports한 객체를 담는다.
const math = require("./Math");
console.dir(math);
math.plus(1, 2);
math.minus(2, 1);
math.division(6, 2);
math.multi(3, 4);
//Math.js
const mathObj = {};
mathObj.plus = (num1, num2) => num1 + num2;
mathObj.minus = (num1, num2) => num1 - num2;
mathObj.multi = (num1, num2) => num1 * num2;
mathObj.division = (num1, num2) => num1 / num2;
module.exports = mathObj;
//index.js
const math = require("./Math");
console.log(math.plus(1, 2)); //3
console.log(math.minus(2, 1)); //1
console.log(math.division(6, 2)); //3
console.log(math.multi(3, 4)); //12
● ES6 모듈화 (import export)
- import , from , export, default 처럼 모듈 관리 전용 키워드를 사용하기 때문에 가독성이 좋다.
- 비동기 방식으로 작동하고 모듈에서 실제로 쓰이는 부분만 불러오기 때문에 성능과 메모리 부분에서 유리하다.
- Common JS에서 지원하지 않는 기능들이 있다.
- Node.js에서 import 를 사용할 떄는 반드시 .js 확장자를 붙여야 한다.
⚬ export
- NodeJS에서 import module을 사용하기 위해서는 다음과 같이 package.json의 수정이 필요하다. type: ‘module’로 변경해주어야 한다. default type은 ‘CommonJs이기에’ require 방식으로 모듈화가 진행되기 때문이다. 이를 변경하지 않으면 다음과 같은 Error를 볼 수 있다. ”SyntaxError: Cannot use import statement outside a module”
{
"name": "deepjs",
"version": "1.0.0",
"main": "ex.js",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"loadsh": "^0.0.4"
},
"type": "module"
}
이는 브라우저에서 script 방식으로 JS 파일을 로드할 때도 같다. JS 파일에 모듈화를 적용하여 이를 브라우저에서 활용하려면 script의 type attribute에 “module” 을 설정해야 한다.
<script src="./src/js/script.js" type="module"></script>
//Math.js
export const plus = (num1, num2) => num1 + num2;
export const minus = (num1, num2) => num1 - num2;
export const multi = (num1, num2) => num1 * num2;
export const division = (num1, num2) => num1 / num2;
//index.js 방법 1
import { plus, minus, multi, division } from "./Math.js";
console.log(plus(1, 2)); //3
console.log(minus(2, 2)); //0
console.log(multi(3, 2)); //6
console.log(division(4, 2)); //2
//index.js 방법 2
import * as math from "./Math.js";
// export로 내보낸 모든 메서드 및 파라미터를 math라는 객체에 저장하여 사용하겠다.
console.log(math.plus(1, 2));
console.log(math.minus(2, 2));
console.log(math.multi(3, 2));
console.log(math.division(4, 2));
⚬ export default
- export default 키워드를 사용하는 경우 var, let, const 키워드는 사용할 수 없다.
- 하나의 파일에서 하나의 값만 export 하는 경우 사용한다.
//Math.js
const Mathobj = {};
Mathobj.plus = (num1, num2) => num1 + num2;
Mathobj.minus = (num1, num2) => num1 - num2;
Mathobj.multi = (num1, num2) => num1 * num2;
Mathobj.division = (num1, num2) => num1 / num2;
export default Mathobj;
// index.js
import Math from "./Math.js";
console.log(Math.plus(1, 2));
console.log(Math.minus(2, 2));
console.log(Math.multi(3, 2));
console.log(Math.division(4, 2));
default 키워드와 함께 export한 모듈은 {} 없이 이름으로 import 한다.
이때 이름은 임의로 정할 수 있지만, 파일의 이름, 혹은 export default한 변수와 동일한 이름으로 호출하는 경우가 일반적이다.
이런 모듈들을 사용하고 의존성을 관리하면서 하나의 모듈로 모아주는 것이 Webpack~~!! 이라는 모듈러가 있다.