home
🖥️

[메디스트림 개발팀] JavaScript Modules

먼저 알려드립니다

index.html , index.js 와 같이 파일명과 확장자가 표시된 코드는 실제로 실행 가능하며, 파일명 그대로 복사하셔서 생성 후, 코드를 복사해서 붙여넣기 하시면 실행이 가능합니다. *표시가 있는 단어는 궁금해요! 앞에 있는 화살표를 클릭하시면, 참고하실 수 있게 부가적인 설명을 보실 수 있습니다.

모듈이란

프로그램을 구성하며 하나의 동작을 수행하는 독립적인 클래스 또는 함수를 말합니다.
여러 가지 기능들에 관한 코드가 모여 있는 하나의 파일을 말하며, 다음과 같은 이유로 사용되고 있습니다. - 유지 보수성 (Maintainability) : 기능들이 모듈화가 잘 되어 있다면, 그만큼 기능 간 의존성을 줄일 수 있기 때문에 기능을 개선하거나 수정할 때 훨씬 편합니다. 기능들이 모듈화가 잘 안되어 있는 경우는 책을 예를 들어 설명하겠습니다. 책의 일 부분(한 챕터)을 수정할 때 다른 모든 부분(챕터)을 수정해야 한다면 참으로 끔찍하겠죠? 당연히 책의 다른 부분에 영향을 끼치지 않고 책의 일 부분(한 챕터)만 수정하고 싶을겁니다. - 네임스페이스화 (Namespacing) : 여러 스크립트가 한 페이지 안에 함께 있는 코드에서는 전역 변수가 많아질 수록 이름이 겹칠 우려가 있습니다. 뿐만 아니라 어느 곳에서든 접근이 가능하므로 코드의 신뢰성을 떨어트릴 수 있습니다. 그러나, 모듈로 분리하면 모듈만의 네임스페이스를 갖기 때문에 신뢰성을 확보할 수 있습니다. - 재사용성 (Reusability) : 똑같은 코드를 새로운 프로젝트 생성 시 또는 전혀 다른 프로젝트에서도 사용할 수 있습니다.

그럼 도대체 모듈의 장점은 무엇일까요? 아래 예제 코드를 통해 살펴보죠

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <script> function helloMedistream() { return 'Hello, Medistream!' } alert(helloMedistream()) </script> </body> </html>
HTML
복사
index.html
예제 코드는 문제 없이 실행이 잘 됩니다.
하지만, helloMedistream 이라는 함수가 자주 사용되는 함수라고 가정을 해봅시다.
이 함수가 실제로는 복잡한 로직을 가지고 있다면, 필요한 html 페이지마다 함수의 전체 로직을 사용하는 것은 낭비이고 유지보수도 힘들게 됩니다.
그럼 이제 이 코드를 모듈화의 개념을 적용해서 조금 수정해 볼까요?
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="helloMedistream.js"></script> </head> <body> <script> alert(helloMedistream()) </script> </body> </html>
HTML
복사
index.html
function helloMedistream() { return 'Hello, Medistream!' }
JavaScript
복사
helloMedistream.js
위와 같이 수정된 예제 코드를 살펴보면,
index.html 파일의 head 태그 안에 <script src="helloMedistream.js></script> 라고 선언해 준 뒤,
body 태그 안에서는 <script>alert(helloMedistream())</script> 라고만 선언해 주면
Hello, Medistream! 이라는 멋진 문구를 볼 수가 있습니다.
다른 문구를 출력하고 싶다면 helloMedistream.js 파일 내 홑따옴표 안에 있는 'Hello, Medistream!' 글자만 수정하면 얼마든지 여러분만의 멋진 문구를 볼 수가 있습니다.
다른 수 많은 html 파일 에서도 head 태그 안에 선언을 해 주고, body 태그 안에서 해당 함수만 호출해 준다면, 어느 파일에서든지 Hello, Medistream! 라는 문구를 볼 수가 있습니다.
이런 모듈의 장점들을 살리기 위해 자바스크립트 커뮤니티는 여러 가지 시도들을 하게 되는데, CommonJS, AMD, UMD 의 등장 배경이 됩니다.

CJS (Common JavaScript)

앞서 인용된 글에서처럼 모듈 시스템이란 것이 없던 상황에서, 자바스크립트를 *클라이언트 사이드 에서 뿐만 아니라 *서버 사이드 에서도 범용적으로 사용하기 위한 움직임이 일어났습니다.
궁금해요!
이 때, 서버 사이드 자바스크립트 *런타임 인 node.js 의 경우 모듈 시스템으로 CJS 방식을 채택했습니다.
따라서, node.js 환경에서는 모듈별로 독립적인 *스코프 를 가진다는 것을 알 수 있습니다.
궁금해요!
그럼 아래 예제 코드를 통해 좀 더 자세히 살펴볼까요?
Hello, Medistream! 를 출력하는 함수를 가진 파일을 helloMedistream.js 라고 하고
이 함수를 가져와서 사용하는 파일을 index.js 라고 한다면 다음과 같이 사용할 수 있습니다.
const helloMedistream = () => { console.log('Hello, Medistream!') } module.exports = { helloMedistream }
JavaScript
복사
helloMedistream.js
const welcome = require('./helloMedistream.js') welcome.helloMedistream()
JavaScript
복사
index.js
helloMedistream.js 파일의 코드를 잠시 살펴보면,
module.exports 에서 module 의 의미는 현재 모듈에 대한 정보를 갖고 있는 객체입니다.
module 뒤에 붙는 exports 도 궁금하시죠?
module 을 예약어 라고 하는데, 다양한 속성 이 있고 exports 객체를 가지고 있습니다.
module.exports 대신 exports를 사용할 수도 있는데, 다음과 같이 몇 가지 주의사항이 있습니다.
module.exports 는 빈 객체 를 참조합니다.
exports 는 module.exports 를 참조합니다.
require 는 항상 module.exports 를 리턴 받습니다.
쉽게 풀어서 설명하면, module.exports 의 속성을 수정하거나 exports 의 속성을 수정하는 것은 동일한 의미를 지니고 있습니다.
그런데, exports 자체에 다른 값을 대입하는 건 허용이 안됩니다.
다른 값을 대입했다는 것은 더 이상 module.exports 객체를 가리키지 않게 되기 때문입니다.
이해하기 쉽게 코드를 통해 좀 더 알아볼까요?
// allowed module.exports = { name: '메디스트림', koreanMedicinePlatform: 'top', rank: 1 } // allowed module.exports = 1 // allowed exports.name = '메디스트림최고' // not allowed exports = 2
JavaScript
복사

AMD (Asynchronous Module Definition)

앞서 살펴보았던 CJS는 *동기적 방식 을 사용하기 때문에, 클라이언트 사이드 입장인 브라우저에서는 필요한 모듈이 모두 다운로드 될 때까지 아무 것도 할 수가 없습니다. 즉, 서버 사이드에서만 사용이 가능합니다.
따라서, 이를 작동시키기 위해서는 webpack 과 같은 *모듈 번들러 를 통한 *모듈 번들링이 필요합니다.
궁금해요!
모듈 시스템이 등장하게 된 배경을 돌이켜 생각해볼까요?
브라우저가 모듈 시스템을 지원하지 않지만, 모듈을 어떻게 사용할까에 대한 고민으로 클라이언트 사이드와 서버 사이드 모두 범용적인 움직임이 일어났습니다.
CJS는 서버 사이드 자바스크립트 모듈 시스템을 사용하기 위한 방법론이라서, '모듈 시스템' 의 고민을 완전히 해결하지 못했다고 볼 수 있습니다.
이를 보완하기 위해, 클라이언트 사이드에서 사용하기 위한 방법론으로 *비동기 방식 을 사용하는 AMD가 등장했습니다.
궁금해요!
아래 예제 코드를 실행하기 위해서,
사이트에서 LATEST RELEASE 부분의
Minified 또는 With Comments 또는 Download 를 눌러 열리는 내용들을 복사해서
require.js 파일을 만들고 붙여넣기 해주시기 바랍니다.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <!-- 이 코드를 실행하기전 아래 사이트에서 r.js 파일을 다운 받아 require.js 로 저장해주세요! --> <!-- require.js 가 먼저 로드된 후, index.js 파일을 실행합니다. --> <script src="require.js" data-main="index.js"></script> </body> </html>
HTML
복사
index.html
require.config({ baseUrl: '/', // 기본경로 지정 paths: { // 모듈명: '모듈에 해당하는 경로' helloMedistream: 'helloMedistream' } }) // 첫 번째 인자에 해당하는 모듈이 로드 되었을 때, helloMedistream 로 받아서, // sayHello 함수를 호출합니다. 의존성 모듈을 지정해주는 거라고 할 수 있습니다. // require(['모듈명'], (받을 인자이름 지정) => { // 지정한 인자.지정함수() // }) require(['helloMedistream'], (helloMedistream) => { helloMedistream.sayHello() })
JavaScript
복사
index.js
define(() => { // 위 index.js 파일에서 require 에 지정해 준 부분과 같이 아래 return 부분이 실행되기전 // 로드될 모듈을 지정해 줄 수 있습니다. (콜백 함수를 작성하여 불러온 모듈 사용 가능) return { sayHello: () => console.log('Hello') } })
JavaScript
복사
helloMedistream.js

UMD (Universal Module Definition)

이해하기 쉽게 앞서 살펴보았던 CJS 와 AMD 를 합친 모듈 시스템이라고 보시면 됩니다.
(엄밀히 말해서 UMD는 모듈 시스템이 아니라 패턴에 가깝습니다.)
이번에는 공식 UMD 소스코드 를 보며 얘기해볼까요?
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['b'], factory); } else if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(require('b')); } else { // Browser globals (root is window) root.returnExports = factory(root.b); } }(typeof self !== 'undefined' ? self : this, function (b) { // Use b in some fashion. // Just return a value to define the module export. // This example returns an object, but the module // can return a function as the exported value. return {}; }));
JavaScript
복사
공식 소스코드
if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['b'], factory); }
JavaScript
복사
AMD 부분
AMD 부분을 살펴보면,
define() 이 함수이고, define.amd 속성의 객체를 가지고 있다는 뜻입니다.
else if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(require('b')); }
JavaScript
복사
CJS 부분
CJS 부분을 살펴보면 module 이 객체이고, module.exports 속성의 객체를 가지고 있다는 뜻입니다.
그럼 UMD 는 어떤 방식으로 사용한다는 걸까요?
(typeof self !== 'undefined' ? self : this, function (b) { // Use b in some fashion. // Just return a value to define the module export. // This example returns an object, but the module // can return a function as the exported value. return {}; })
JavaScript
복사
UMD 방식
(function (root, factory) {
JavaScript
복사
코드 첫 부분
코드 첫 부분에 보면, 2개의 인자를 전달받는 함수를 실행하는 겁니다.
첫 번째 인자 root 는, 클라이언트 사이드 에서 구현할 값을 undefined 가 아니라면 self 맞다면 this 를 리턴한다는 얘기인데 쉽게 말해서 window 객체에 전부 넣어서 리턴하는 함수를 보냅니다.

ESM (ECMAScript Module)

도대체 CJS, AMD, UMD 어떤걸 사용해야 하는 걸까요?
모듈을 사용하기 위해 위와 같은 시스템을 위한 코드를 별도로 작성하는 것은 번거롭습니다.
이런 상황에서 자바스크립트 자체에서 모듈을 지원하는 것이 좋겠다는 움직임이 있었고, ES6 이후로는 자체에서 아래 코드에서 보는 바와 같이 모듈 시스템을 지원하기 시작했습니다.
const helloMedistream = () => 'helloMedistream' export default helloMedistream
JavaScript
복사
모듈
import helloMedistream from '모듈'
JavaScript
복사
인덱스

정리

1.
CJS (CommonJS)에서 패키지 개념을 처음 제안하여 node.js 에서 사실상 표준으로 먼저 자리 잡았습니다.
2.
AMD (Asynchronous Module Definition) CommonJS 에서 분리된 프로젝트로 클라이언트 사이드에서 사용하기 위한 방법론으로 비동기 개념을 추가한 개념입니다.
3.
UMD (Universal Module Definition) AMD와 CommonJS 모두 사용 가능합니다. AMD는 클라이언트 사이드에서 많이 사용되고, CommonJS는 서버 사이드에서 많이 사용되기 때문에, UMD를 사용하면 두 개의 모듈을 따로 만들 필요가 없습니다.
4.
ESM (ECMAScript Module) ES6 자바스크립트 모듈 기능으로 추가되어 공식적인 표준을 제공하고 있습니다.

마치며

지금까지 자바스크립트 모듈이 왜 도입되어야 했고, 여러 사람들이 어떤 방식으로 고민을 했는지 사용법과 함께 간단히 알아보았습니다. 이를 통해서, 서버 사이드 또는 클라이언트 사이드를 개발할 때 적절한 방식을 채택해서 사용하면 보다 효과적일 것 같다는 생각을 해봅니다.
긴 글 끝까지 읽어주셔서 대단히 감사드립니다!!
이상 메디스트림 프론트엔드 개발자 정주환이었습니다.

References