자바스크립트 모듈러 프로그래밍(Modular Programing)
모듈러 프로그래밍(Modular Programming)
먼저 모듈(Module)이란 무엇일까요. 블록을 조립하면서 만드는 레고(Lego)를 생각해 봅시다.
기본적으로는 직육면체나 정육면체 모양의 브릭(Brick) 조각들을 이용해 원하는 모양으로 조립해 나가며 원하는 모습을 만들어 나갑니다. 브릭 조각들로 지붕 모양을 만들었다면, 집 모양을 만든 후에 지붕을 떼서 만든 집에 얹을 수도 있고, 다른 집으로 옮길 수도 있습니다. 이른바 재활용이라고 볼 수 있겠지요.
브릭으로는 사람 모양을 만들 수는 있겠지만, 미리 사람 모양으로 만들어진 사람 블록인 미니 피겨(Mini Figure)를 사용할 수도 있습니다. 또 제품에 따라서는 헬리콥터의 프로펠러나, 특정 영화, 애니메이션의 모양으로 미리 만들어진 조립된 블록들이 있기도 하구요. 이런 것들을 하나의 모듈이라고 볼 수 있습니다.
사전에서 Modular를 검색해보면, “(규격화된 부분을 조립하여 만드는) 조립식의”라는 뜻을 갖는데, 위의 레고의 예시에서 브릭 등을 이용해 조립한 것을 하나의 모듈이라고 볼 수 있습니다. 이를 프로그래밍으로 가져와보면, 브릭은 프로그래밍 언어를 구성하는 문법의 조각들로 볼 수 있고, 직접 조립하거나 판매되는 조립된 제품을 모듈로 생각할 수 있습니다.
소프트웨어 공학에서는 규모가 큰 작업을 할 때 ‘분할과 정복(Divide & Conquer)’의 방법으로 접근합니다. 큰 작업을 작은 단위의 작업으로 나누고 이를 모아 하나의 시스템으로 조립해 나갑니다. 이러한 접근 방법을 프로그래밍 언어에서는 모듈러 프로그래밍을 지원하여, 복잡하고 큰 문제를 단순하게 만들고, 또 재활용성을 높여 효율적인 개발이 될 수 있도록 합니다.
자바스크립트는 C, Java와 같은 범용 프로그래밍 언어들과 달리 웹 브라우저 내에서 HTML의 조작을 위해 만들어진 언어이다보니, 주요 언어들에서 제공하는 기능들이 많이 배제되어 있었습니다. 모듈 시스템 역시 ES6 이전의 자바스크립트에서는 지원되지 않았습니다.
왜 모듈이 필요할까요?
아래의 코드를 잠깐 볼까요? “몇 글자?” 버튼을 누르면 텍스트 입력 박스에 몇 글자가 입력되었는지 경고창으로 보여주는 페이지입니다.
<!-- index.html 파일 -->
<html>
<body>
<input id="message"> <button id="buttonLength">몇 글자?</button>
<script>
// 문자열의 길이를 반환하는 함수
function getLength(message) {
return message.length;
}
document.getElementById('buttonLength').onclick = function() {
var message = document.getElementById('message').value;
alert(getLength(message));
}
</script>
</body>
</html>
문자열의 길이를 반환하는 getLength
함수를 선언하고 버튼 클릭 시 이를 호출하고 있습니다. 만약 getLength
함수를 이 페이지가 아닌 다른 페이지에서도 계속해서 쓰게 된다면 어떻게 해야할까요?
가장 간단한 방법으로는 모든 페이지마다 getLength
함수를 선언해주는 방법이 있겠죠. 물론 getLength
함수는 짧고 간단해서 그렇게 작업하는데 어려움이 따르진 않겠지만, 우리는 ‘복붙’이 좋은 방법이 아닌 것을 알고 있습니다.
그 다음 방법으로는 별도의 js
파일을 만들어서 해당 파일에 getLength
를 선언하고 <script>
태그로 불러오는 방법이 있습니다. 그럼 아래와 같은 모습이 되겠죠?
// util.js
function getLength(message) {
return message.length;
}
<!-- index.html 파일 -->
<html>
<head>
<!-- util.js 파일을 불러옵니다. -->
<script src="./util.js"></script>
</head>
<body>
<input id="message"> <button id="buttonLength">몇 글자?</button>
<script>
document.getElementById('buttonLength').onclick = function() {
var message = document.getElementById('message').value;
alert(getLength(message));
}
</script>
</body>
</html>
이제 getLength
함수가 필요한 페이지에서는 언제든지 util.js
파일을 불러와 사용할 수 있습니다. 지금은 하나의 함수밖에 없지만 유용한 함수들을 util.js
파일에 담는다면 생산성이 높아질 수 있겠죠?
프로젝트가 점점 커지면서, util.js
와 같이 다른 사람들이 작성한 자바스크립트 파일을 불러와 사용하게 되었습니다. 페이지에서 그림을 그리는 기능이 추가되면서, 그래픽 관련한 함수들을 작성한 graphic.js
라는 파일도 함께 사용하게 되었습니다.
<!-- index.html 파일 -->
<html>
<head>
<!-- util.js 파일을 불러옵니다. -->
<script src="./util.js"></script>
<!-- graphic.js 파일을 불러옵니다. -->
<script src="./graphic.js"></script>
</head>
<body>
<input id="message"> <button id="buttonLength">몇 글자?</button>
<script>
document.getElementById('buttonLength').onclick = function() {
var message = document.getElementById('message').value;
alert(getLength(message));
}
</script>
</body>
</html>
index.html
파일에 <script src="./graphic.js"></script>
부분이 추가되었습니다. 그런데 하필이면 graphic.js
파일에 직선의 양 끝 점을 파라미터로 받아 직선의 길이를 구하는 getLength
라는 이름의 함수가 있네요.
<위키피디아 - 두 점 사이의 거리>
// graphic.js
// point 객체는 x,y 좌표의 값을 갖고 있습니다.
// point = { x: 1.5, y: 24.3 }
function getLength(point1, point2) {
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
}
util.js
와 graphic.js
파일에 둘 다 getLength
라는 함수가 있고, index.html
파일에서는 이 두 파일을 모두 불러오고 있습니다.
그럼 버튼 클릭 이벤트 리스너에서 호출하는 alert(getLength(message))
는 무엇을 호출하게 될까요?
<script>
태그로 불러오는 순서를 기준으로 했을 때 뒤쪽에 있는 graphic.js
의 getLength
함수가 호출됩니다. 그럼 원래 index.html
페이지의 버튼에서 글자 수를 가져올 것이라고 기대했지만, 원하던 결과가 나오지 않게됩니다.
이는 두 자바스크립트 파일이 같은 네임스페이스(이름 공간, Namespace)를 사용하고 있고, 웹 브라우저의 자바스크립트는 같은 네임스페이스에 같은 이름을 가진 경우에 덮어써버리기 때문입니다.
.js
파일로 분리한 것도 모듈화한 것으로 볼 수 있지만, 각각의 파일에서 네임스페이스가 분리되지 않고 전역 공간(브라우저에서는 window
객체)을 함께 사용하게 되어 문제가 발생할 수 있습니다.
물론 개발하는 사람들과 규칙을 잘 정해서 이를 피할 수는 있겠지만 언어 차원에서 이를 방지하지 않는다면 언제라도 발생할 수 있는 문제입니다.
(모듈과 별개로 자바스크립트에서 이러한 네임 스페이스 충돌 문제를 해결하기 위한 여러가지 방법이 있습니다. - Nextree 네임스페이스 패턴)
점점 웹 페이지가 복잡해지고 전역 네임스페이스를 기반으로 하는 자바스크립트의 근본적인 문제점으로 인해 모듈 시스템을 표준화하고, 이를 구현한 다양한 라이브러리들이 등장했습니다. 자바스크립트에서의 모듈화를 표준화하려는 노력의 결과로 크게 AMD와 CommonJS가 있고, ES6부터 공식적으로 언어의 규격으로 자리잡았습니다.
- AMD(Asynchronous Module Definition): 모듈을 비동기적으로 로드하며, 브라우저 자바스크립트를 위해 설계되었습니다. 주요 모듈 러너 구현으로
Require.js
가 있습니다. - CommonJS: 서버 측 자바스크립트를 위해 설계되었으며,
Node.js
와Io.js
에서 사용됩니다. - ES6 Module: 자바스크립트 표준 모듈. Node.js는 8.5.0부터 지원하기 시작했으나 글을 쓰는 현재(13.x)까지 아직 실험적(Experimental)으로 지원합니다. 이에 따라
.js
가 아닌.mjs
확장자를 갖는 자바스크립트 파일로 작성되어야 하고 또한 실행 시--experimental-modules
옵션을 주어야 합니다.
모듈러 프로그래밍의 장점
자바스크립트에서 모듈러 프로그래밍을 하면 어떤 장점이 있을까요?
-
전역 네임스페이스의 오염(충돌) 방지
모듈마다 각각의 네임스페이스를 갖게 되므로, 전역 네임스페이스가 더렵혀지지 않으므로, 문제가 발생할 수 있는 가능성을 사전에 방지할 수 있습니다. -
파일의 분할(코드의 구조화)
모든 자바스크립트 함수, 클래스 등이 하나의 파일에 들어있는 모습을 상상해보세요. 물론 작은 규모라면 크게 문제가 되지 않을 수도 있지만, 100라인, 1000라인, 10000라인… 규모가 커질 수록 관리가 어려워지고, 수정이 필요한 코드를 찾는데도 어려움이 따를 수 밖에 없습니다. 마치 도서관의 책들이 분류 없이 한 곳에 꽂혀 있는데 원하는 책을 찾고자 할 때의 모습이라고 볼 수 있습니다. 파일로 분할된다는 것은 이와 함께 폴더도 나눌 수 있게 되므로, 잘 나뉘어진 폴더와 파일은 필요한 코드를 쉽게 찾고 관리할 수 있음을 의미합니다. -
재사용
위의index.html
의 예시에서 봤듯 모듈을 분리하여 해당 모듈을 필요로 하는 곳에서 불러와서 다시 재사용할 수 있게 됩니다. 같은 코드를 다시 작성하지 않아도 된다는 의미이기도 하구요. 물론 그러한 반복 작업을 좋아할 수도 있겠지만, 만약 같은 코드를 여러 곳에서 작성하고 있다면, 해당 코드가 바뀌었을 때 모든 부분을 동일하게 바꿔줘야 합니다. 사람은 완벽하지 않습니다. 어느 한 부분을 빼먹을 수도 있죠.
ES6 모듈
ES6부터는 공식적으로 언어 자체적으로 모듈 시스템을 제공합니다. ECMAScript 2015 - ECMA-262에서 명세를 확인할 수 있습니다.
ES6의 모듈은 .js
확장자를 갖는 하나의 파일로 분리하여 작성하며 export
로 모듈에서 내보낼 것을 지정하고, import
로 내보낸 것을 가져와 사용하게 됩니다.
export
를 이용해 모듈의 변수, 함수, 클래스 등을 내보낼 수 있고, 내보낼 때는 이름을 갖는(named) 내보내기와, 기본(default) 내보내기가 있습니다.
이름을 갖는 내보내기
// my-module.js 파일
// PI 라는 이름의 상수 내보내기
export const PI = 3.14;
// getLength라는 이름의 함수 내보내기
export function getLength(message) {
return message.length;
}
// Point 클래스 내보내기
export class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
위에서 작성한 my-module.js
모듈은 아래와 같이 가져와서 사용할 수 있습니다.
// my-module에서 내보낸 PI와 getLength만을 가져옵니다(import).
// { PI, getLength } 부분은 ES6의 비구조화 문법입니다.
import { PI, getLength } from './my-module.js';
console.log(PI); // PI 값 출력
console.log(getLength('안녕하세요')); // 문자열의 길이인 5 출력
my-module.js
에서 이름을 주어 내보낸 모든 것들을 가지고 오려면 아래와 같이 쓸 수 있습니다.
import * as myModule from './my-module.js';
console.log(myModule.PI); // PI 값 출력
console.dir(new myModule.Point(0, 0)); // Point 객체 출력
이름을 갖지 않는 기본(default) 내보내기는 export default ...
의 형식으로 내보낼 수 있고, 모듈에서 기본 내보내기는 한 번만 가능합니다.
// my-module.js 파일
// ...윗 부분 생략
// 이름 없이 '안녕하세요' 라는 문자열만 내보냅니다.
export default '안녕하세요';
기본 내보내기는 이름이 있는 내보내기를 가져올 때와 달리 중괄호 없이 가져옵니다.
import message from './my-module.js';
console.log(message); // 안녕하세요 출력
기본 내보내기와 이름 있는 내보내기를 함께 가져오려면 아래와 같이 작성합니다.
import message, { PI, Point } from './my-module.js';
console.log(message); // 안녕하세요 출력
console.log(PI); // PI 값 출력
이름을 주고 내보낸 것들을 모두 가져오려면 아래와 같이 작성합니다.
import message, * as myModule from './my-module.js';
console.log(message); // 안녕하세요 출력
console.log(myModule.PI); // PI 값 출력
간단히 export
와 import
를 알아보았습니다. 더 상세한 내용은 MDN의 export와 import 문서를 참고해 주세요.
브라우저에서 ES6 모듈 사용해보기
Internet Explorer를 제외한 대부분의 주요 브라우저에서 ES6 모듈을 지원합니다. import
구문은 아래와 같이 지원됩니다.
<import 구문의 브라우저 호환성> - https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import
앞서 getLength
예시에 ES6 모듈을 적용해 보겠습니다. 먼저 util.js
와 graphic.js
에 export
로 getLength
를 내보내 봅시다.
(그 전에 아래 예제를 따라하다보면 Access to script at 'file:///.../util.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
와 같은 메시지와 함께 정상적으로 모듈이 불러와지지 않습니다.
이를 위해 Node로 로컬 서버 실행하기 문서를 먼저 읽고 예제를 따라서 실습해 봅시다.)
util.js
파일
export function getLength(message) {
return message.length;
}
graphic.js
파일
// point 객체는 x,y 좌표의 값을 갖고 있습니다.
// point = { x: 1.5, y: 24.3 }
export function getLength(point1, point2) {
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
}
두 파일의 getLength
함수 앞에 export
를 붙여 내보내기를 적용했습니다. 이를 index.html
파일에서 사용해 봅시다.
브라우저의 <script>
태그에서 모듈을 사용하려면 type="module"
속성을 태그에 추가해주어야 합니다. 이제 모듈은 import
를 통해 불러오게 되므로 util.js
와 graphic.js
를 <script>
태그로 불러올 필요가 없어졌으므로 이를 지워줍니다.
<!-- index.html 파일 -->
<html>
<head>
<!-- js 파일을 불러오는 부분이 지워졌습니다.. -->
</head>
<body>
<input id="message"> <button id="buttonLength">몇 글자?</button>
<!--
script 태그에 type="module" 속성이 추가되었습니다. 이는 브라우저에 모듈 시스템을 사용한다는 것을 알립니다.
type="module"을 추가하지 않으면 import와 export 사용 시 오류가 발생합니다.
-->
<script type="module">
// util.js의 getLength를 가져옵니다.
import { getLength } from './util.js';
// graphic.js의 getLength를 로드하더라도 import에서 다른 이름을 줄 수 있습니다.
import { getLength as getLineLength } from './graphic.js';
document.getElementById('buttonLength').onclick = function() {
var message = document.getElementById('message').value;
alert(getLength(message));
}
</script>
</body>
</html>
이제 다시 “몇 글자?” 버튼을 누르면 정상적으로 입력란에 입력한 글자 수를 가져오게 됩니다. 브라우저에서 ES6 모듈을 사용할 때는 위에 언급했듯 type="module"
속성을 주어야 하고, 또한 인라인 자바스크립트에서는 export
를 할 수 없습니다.
ES6의 모듈을 브라우저에서 간단하게 사용할 수 있지만 문제는 호환성에 있습니다. 모든 사용자가 ES6 모듈을 지원하는 브라우저를 사용하지는 않으므로, 이에 대한 대안이 필요하며, 이를 위해 ES6으로 작성한 자바스크립트 파일을 트랜스파일(Transpile)하고, 번들링하여 브라우저 호환성을 높일 수 있도록 합니다. 트랜스파일링과 번들링은 다른 포스트에서 다뤄보도록 하겠습니다.