자바스크립트로 어느 정도 규모가 있는 애플리케이션을 만들다 보면 자연스레 용도에 맞게 자바스크립트의 파일들이 분리되어집니다. 이렇게 분리된 파일을 각각 모듈(module)이라고 부르는데, 모듈은 대개 클래스 하나 혹은 다중의 함수들로 구성된 라이브러리로 구성됩니다.
이전에야 자바스크립트는 단순히 웹을 역동적으로 만들어주는 역할만으로 그쳤기에 module이라는 개념자체가 필요 없었을 것입니다. 하지만 자바스크립트의 크기가 점진적으로 늘어나고, 기능도 복잡해짐에 따라 자바스크립트 커뮤니티에는 모듈을 호출하거나, 모듈로 분리되는 다양한 시도를 했습니다.
이러한 시도는 다음과 같은 모듈 시스템으로 이어졌습니다.
- AMD – 가장 오래된 모듈 시스템 중 하나로 require.js라는 라이브러리를 통해 처음 개발되었습니다.
- CommonJS – Node.js 서버를 위해 만들어진 모듈 시스템입니다.
- UMD – AMD와 CommonJS와 같은 다양한 모듈 시스템을 함께 사용하기 위해 만들어졌습니다.
이런 모듈 시스템은 레거시 코드에 종종 발견할 수 있지만. 이제는 더이상 보기 힘들지도 모릅니다.
2015년 모듈 시스템은 ES6의 발표와 함께 표준으로 발표되었습니다.
모듈은 단지 개발자에의해 목적을 가지고 분리된 파일 하나에 불과할 뿐입니다. 이 모듈에 export와 improt를 적용하면 다른 모듈을 불러와, 불러온 모둘에 있는 함수를 호출하는 것과 같은 기능 공유가 가능합니다.
- export : 지시자를 변수나, 함수, 클래스 앞에 붙이면,
외부 모듈에서 해당 변수나, 함수, 클래스에 접근할 수 있습니다. - import : 지시자를 사용하면, 외부 모듈의 기능을 가져올 수 있습니다.
// my.js
export function myFunc (args) {
console.log(`hi ${args}`);
}
my.js파일에서 myFun이라는 함수는 앞에 export라는 지시자를 붙임으로서 외부에서도 접근할 수 있게 되었습니다.
import { myFunc } from './my.js' ;
다음과 같이 외부 모듈에서는 다음과 같은 import구문으로 my.js 파일에 존재하는 함수에 접근할 수 있습니다.
위 예시에서 import 지시자는 상대 경로 기준으로 모듈을 가져오고, my.js파일에서 내보낸 함수 myFunc에 상응하는 변수에 할당합니다.
이러한 모듈을 브라우저에게 전달할 때 해당 스크립트가 모듈이란 걸 브라우저에게 알려주어야 합니다.
<script type="module"></script>
! 모듈은 로컬 파일에서 동작하지 않고, HTTP 또는 HTTPS 프로토콜을 통해서만 동작합니다.
local에서 file:// 프로토콜을 사용해 웹페이지를 열면, import, export 지시자가 동작하지 않습니다.
모든 호스트 환경에서 공통으로 적용되는 모듈의 핵심 기능에 대해 알아보자.
- 항상 use strict 모드로 실행된다.
- 모든 모듈은 자신만의 scope를 가지고 있으며, 따라서 모듈 내부에서 정의한 변수나 함수는 다른 스크립트에 접근할 수 없습니다.
- 외부에 공개하려는 모듈은 export 지시어를 앞에 붙어주어야 하며, 내보내진 모듈을 가져와 사용하려면 import를 사용해야 합니다.
- 브라우저 환경에서도 <script type="module">을 만들면 독립적인 스코프가 만들어집니다.
- 동일한 모듈이 여러 곳에서 사용되더라도, 모듈은 최초 호출 시 단 한 번만 실행됩니다. 실행 후 결과는 이 모듈을 import하는 모든 모듈에 내보내집니다. 보통 최상위 레벨 모듈을 대개 초기화나 내부에서 쓰이는 데이터 구조를 만들고 이를 내보내 재사용하고 싶을 때 사용합니다. (um... 모듈이 여러곳에서 호출되어도 실행은 한번만 하기 때문에, 초기화 시 생성된 객체나, 변수들은 전역 설정으로 처리가 된다는 걸까? )
- 그렇다 해당 모듈이 한번 초기화된다는 것은 안에 정의된 객체들을 메모리값을 공유한다는 것이기 때문에, 각기 다른 모듈에서 값을 수정하면 그 파장은 여러 곳에 퍼질 것이다.
ex) 전역 모듈에서 초기화한 객체를 다른 모듈에서 수정한 경우
// init module
// init.js
export const donEditObj = {
name : 'sung il',
age : 27,
}
// a module
// a.js
import { donEditObj } './init.js';
donEditObj.name = 'edit NAME';
// b module
// b.js
import { donEditObj } './init.js';
console.log( donEditObj.name); // edit NAME
각 모듈에 동일한 객체가 전달되는 것을 확인할 수 있다. 내 생각에는 객체를 전달하는 것은 올바른 모듈 사용 법이 아닌 것 같다. 객체를 정의한 class를 export로 내보내 주어 다른 모듈에서 필요에 따라 해당 클래스의 인스턴스를 사용하는 것이 바람직한 것 같다. 그렇지만 설정과 같은 값들은 모든 프로퍼티에 값을 세팅한 후 하위 모듈에 전달한다고 생각했을 때 확실히 모듈화에 장점을 살릴 수도 있다.
import.meta
import.meta 객체는 현재 모듈에 대한 정보를 제공합니다. 각 호스트 환경에 따라 제공하는 정보의 내용을 다른데, 브라우저 환경에서는 스크립트의 URL 정보를 얻을 수 있습니다.

인라인 스크립트에서 콘솔로 찍어보니 url 프로퍼티만 가진 객체더라...
this는 undefined
일반적인 자바스크립트 파일이 웹브라우저 환경에서 실행될 때 this는 window의 전역 객체를 의미합니다.
하지만 최상위 레벨의 this는 undefined입니다.
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
지연 실행
모듈 스크립트는 항상 지연 실행됩니다.
지연 실행을 알기 전 defer, async 스크립트에 대해 이해가 필요합니다.
모던 웹브라우저에서 돌아가는 스크립트들은 대부분 HTML보다 무겁습니다. 용량이 매우 커서 다운로드하는데, HTML, CSS 보다 오랜 시간이 걸리고, 처리하는 것 또한 오랜 시간이 걸립니다. 브라우저는 html 파일을 읽다가, <script> 태그를 만나면 스크립트를 실행하기 위해 DOM 생성을 멈춥니다. 이는 src 속성이 있는 외부 스크립트를 만났을 때도 마찬가지입니다. 외부 스크립트를 다운로드하고 실행한 후에야 남은 페이지를 렌더링 합니다..
이러한 브라우저의 동작 방식은 두 가지 이슈를 만들어 낼 수 있습니다.
1. 스크립트에서는 스크립트 아래에 있는 DOM 요소에 접근할 수 없습니다. 따라서 DOM 요소에 접근, 핸들러 설정을 할 때 스크립트 아래에 접근하는 코드가 존재한다면, 오류가 발생할 것입니다.
2. HTML 코드 상단에 용량이 매우 큰 스크립트가 존재한다면, 스크립트 로딩으로 인해 페이지 렌더링이 이루어지지 않는다.
이러한 부작용을 피하기 위해서 여러 가지 방법들이 존재합니다.
스크립트 자체를 가장 최하단에 놓고 렌더링 이후 스크립트를 가져오도록 하는 방법이 있지만 이 방법은 좋은 방법이 아닐 수 있습니다. 렌더링 후 용량이 큰 스크립트를 로딩하고 있는 도중에는 사용자의 행동에 반응이 없을 수 있습니다.
또한, 페이지가 매우 느려질 수도 있습니다.
그래서 이런 문제를 해결할 수 있는 <script> 태그의 속성이 바로 defer와 async입니다.
defer
브라우저는 defer 속성이 있는 스크립트는 백그라운드에서 다운로드합니다. 따라서 지연 스크립트를 다운로드하는 도중에도 HTML 파싱이 멈추지 않습니다. 그리고 defer 스크립트 실행은 페이지 구성이 끝날 때까지 지연됩니다.
<script defer src="url~~"></script>
- 지연 스크립트는 페이지 생성을 절대 막지 않습니다.
- 지연 스크립트는 DOM이 준비된 후에 실행되긴 하지만 DOMContentLoaded 이벤트 발생 전에 실행됩니다.
* DOMContentLoaded : 초기 HTML 문서를 완전히 불러오고 분석했을 때 발생합니다. 스타일시트, 이미지, 하위 프레임의 로딩은 기다리지 않습니다. 즉 DOM 트리를 만드는 즉시 발생합니다. 그렇기 때문에 DOM 트리가 완성되고, 지연 스크립트가 실행된 후 이벤트가 실행됩니다.
* defer 속성은 src 속성이 있어야만 유효합니다.
async
async 속성이 붙은 스크립트는 페이지와 완전히 독립적으로 동작합니다.
defer 스크립트와 동일하게 마찬가지로 백그라운드에서 다운로드됩니다. 하지만 async 스크립트 실행 중에는 HTML 파싱이 멈춥니다.
DOMContentLoaded 이벤트와 async 스크립트는 서로를 기다리지 않습니다.
페이지 구성이 끝난 후에 async 스크립트 다운로딩이 끝난 경우, DOMContentLoaded는 async 스크립트 실행 전에 발생할 수 있습니다. 혹은 async 스크립트가 짧아서 페이지 구성이 끝나기 전에 다운로드되거나, 스크립트가 캐싱 처리된 경우 DOMContentLoaded는 async 스크립트 실행한 후에 발생할 수 있습니다.
다른 스크립트들은 async 스크립트를 기다리지 않습니다. async 스크립트 역시 다른 스크립트들을 기다리지 않습니다.
즉 async는 정확한 순서를 예측할 수 없다. 또한 비동기 스크립트는 서로를 기다리지 않기 때문에 순서상 앞에 위치했다 해도, 먼저 다운로드가 되는 순간 실행됩니다. (load-first order) 이와 반대로 defer는 순서를 기다립니다.
async는 서브 파티 스크립트를 반영할 때 매우 유용합니다. 각 요소에 영향을 미치지 않으면서, 처리됩니다. 우리가 보통 광고를 넣을 때 <script async src="url~">를 넣는 것과 같습니다.
다시 돌아와서 지연 실행에 대해서 봅시다.
모듈은 항상 defer 속성과 같은 행위를 한다고 했습니다.
따라서 모듈을 다운로드할 때 브라우저의 DOM 객체의 파싱이 멈추지 않습니다.
또한 HTML 문서가 완전히 만들어진 이후에 실행됩니다. 또한 스크립트의 상대적 순서가 유지됩니다.
모듈 스크립트를 로딩할 때 병렬적으로 DOM과 script가 다운로드되고, dom완료 후 script가 실행됩니다. 다만 HTML 페이지가 나타난 이후 모듈이 실행되기 때문에, 사용자에게 혼란을 줄 수 있습니다. 이러한 경우는 투명 오버레이로 가리거나, loading indicator를 통해 로딩 중을 알려야 합니다.
Asynchronous processing of inline scripts
모듈이 아닌 일반 스크립트에서 async 속성은 외부 스크립트를 불러올 때만 유효합니다. async 스크립트는 로딩이 끝나면 다른 스크립트나 HTML 문서가 처리되길 기다리지 않고 바로 실행됩니다.
반면 모듈 스크립트의 async 속성을 인라인 스크립트에 적용할 수 있습니다. src가 없어도 되며, 다른 스크립트나, html 파싱이 끝나지 않아도 실행할 수 있는 것이 장점입니다.
경로가 없는 모듈은 금지됩니다.
브라우저 환경에서 import는 반드시 상대/절대 URL 앞에 와야 합니다. 경로가 없는 모듈은 허용되지 않습니다.
Node.js 나 번들링 툴은 경로가 없어도 해당 모듈을 찾을 수 있는 방법을 알기 때문에 사용할 수 있지만 브라우저는 그렇지 않습니다.!
호환성
구식 브라우저 type="module"을 해석하지 못할 수 있기 때문에 이를 무시하고 넘어갈 수 있습니다.
이때는 nomodule 속성을 사용하면 이러한 상황을 대비할 수 있습니다.
<script type="module">
alert("모던 브라우저를 사용하고 계시군요.");
</script>
<script nomodule>
alert("type=module을 해석할 수 있는 브라우저는 nomodule 타입의 스크립트는 넘어갑니다. 따라서 이 alert 문은 실행되지 않습니다.")
alert("오래된 브라우저를 사용하고 있다면 type=module이 붙은 스크립트는 무시됩니다. 대신 이 alert 문이 실행됩니다.");
</script>
build tool
사실 모듈을 브라우저에서 단독으로 사용하는 경우는 흔치 않습니다. 보통 parcel, webpack 등과 같은 번들링 툴을 사용합니다. 번들러를 사용하면 모듈 분해를 통제할 수 있습니다. 여기에 경로가 없는 모듈이나, CSS, HTML 포맷의 모듈을 사용할 수 있게 해 준다는 장점이 있습니다.
빌드 툴의 역할은 아래와 같습니다.
- HTML의 <script type="module">에 넣을 ‘주요(main)’ 모듈(‘진입점’ 역할을 하는 모듈)를 선택합니다.
- ‘주요’ 모듈에 의존하고 있는 모듈 분석을 시작으로 모듈 간의 의존 관계를 파악합니다.
- 모듈 전체를 한데 모아 하나의 큰 파일을 만듭니다(설정에 따라 여러 개의 파일을 만드는 것도 가능합니다). 이 과정에서 import문이 번들러 내 함수로 대체되므로 기존 기능은 그대로 유지됩니다.
- 이런 과정 중에 변형이나 최적화도 함께 수행됩니다.
- 도달 가능하지 않은 코드는 삭제됩니다.
- 내보내진 모듈 중 쓰임처가 없는 모듈을 삭제합니다(가지치기(tree-shaking)).
- console, debugger 같은 개발 관련 코드를 삭제합니다.
- 최신 자바스크립트 문법이 사용된 경우 바벨(Babel)을 사용해 동일한 기능을 하는 낮은 버전의 스크립트로 변환합니다. -> 사랑
- 공백 제거, 변수 이름 줄이기 등으로 산출물의 크기를 줄입니다.
이렇게 번들링이 거쳐진 스크립트는 type="module" 이 필요 없어지기에 일반 스크립트처럼 취급할 수 있습니다.
'JavaScript' 카테고리의 다른 글
| Number Property (0) | 2021.11.22 |
|---|---|
| 구조 분해 할당 (0) | 2021.10.23 |
| Fetch API (0) | 2021.10.17 |
| Fetch API Response (0) | 2021.10.16 |
| Fetch API Request (0) | 2021.10.16 |
댓글