본문 바로가기
Archive

JS 모듈

by livemehere 2022. 9. 4.

모듈은 내가 js를 하면서 가장 관심있게 공부했던 주제이다.

눈으로 위에서 아래로 읽어나가면서 직관적으로 흐름이 파악되는 코드작성하는 것을 좋아하는데,

그 중심에는 선언적 프로그래밍과 모듈이 있었다.

 

주로 순수함수를 여러개 만들어서 레고조립하듯이 했는데, 어느순간부터는 객체로 관리가 필요한 시점이왔고,

Array, Math 와 같이 비슷한 기능을 하는것을 모듈로 만들어보고 싶었다.

이번에 재대로 한번 더 공부해 볼 수있어서 설렌다

 

태초의 자바스크립트

는 모듈기능이 없었다.

그러나 점차 스크립트의 크기가 커지고 기능이 다양해지면서 특별한 라이브러리들이 공유되기 시작했고, 모듈시스템을 도입하게 되었다.

 

대표적으로

1. AMD - requireJS라는 라이브러리를 통해 개발

2. CommonJS - node에서 사용되기 위해 만들어진 모듈 시스템

3. UMD

정도가 있다

 

모듈이란

모듈은 단지 하나의 스크립트 파일을 의미한다.

export, import 지시자를 통해서 모듈간(다른 파일간) 함수를 호출하는 것과같은 기능 공유가 가능하다.

하지만 브라우저에서 스크립트는 모두 공유되는 상태이기 때문에, type=module로 지정해주어야 한다.

 

모듈 시스템을 사용하려면 file:// 프로토콜은 불가능하다

보통은 dev server를 통해서 개발을 하기 때문에 신경쓸 일이 없었다.

브라우저에서는 file 시스템에 접근할 수 없으니까!

 

모듈의 핵심 기능

- 엄격모드로 실행된다 "use strict"

- 모듈레벨 스코프를 가지기 때문에, 다른 스크립트에서 접근할 수 없다.

* 전역 변수가 필요하다면 window객체에 변수를 할당하고, 접근하면되지만 정말 필요한 경우에만 권유된다.

 

- 단 한번만 평가된다

* 동일한 모듈을 여러곳에서 사용하더라도, 모듈은 최초 호출시 단 한번만 실행된다. 즉, js에서 모듈은 기본적으로 singletone으로 동작한다. 하지만 이점이 의문이 생겨서 찾아봤는데, singletone임을 100프로 보장하지는 않는다고한다.

아래와 같이 객체를 조작하면 다른 모듈에서도 그대로 적용된다.

// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";

// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete

// 1.js와 2.js 모두 같은 객체를 가져오므로
// 1.js에서 객체에 가한 조작을 2.js에서도 확인할 수 있습니다.

- import.meta 는 호스트환경에 따라서 내용이 다르지만, 브라우저 환경에서는 스크립트의 url 정보를 얻을 수 있다.

<script type="module">
  alert(import.meta.url); // script URL (인라인 스크립트가 위치해 있는 html 페이지의 URL)
</script>

- this는 window를 가리키지 않는다

명시적으로 window를 호출해야한다

<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>

- 모듈 스크립트는 지연실행된다

* 마치 defer 속성을 붙인것 처럼 실행된다. 그래서 script가 최상단에 와도 문제가업다.

* 스크립트를 읽을 때 멈추지 않고, 나머지 태그를 모두 읽고, HTML 문서가 준비되면 그 때 실행한다. 즉, 파싱은 병렬로 이루어진다.

 

- async 속성으로 html이 처리되기 이전에 실행 할수도 있다

<!-- 필요한 모듈(analytics.js)의 로드가 끝나면 -->
<!-- 문서나 다른 <script>가 로드되길 기다리지 않고 바로 실행됩니다.-->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

 

- 동일한 외부 스크립트는 한번만 실행된다

<!-- my.js는 한 번만 로드 및 실행됩니다. -->
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>

 

- 외부 사이트에서 받아오는 모듈 스크립트는 CORS헤더가 필요하다

<!-- another-site.com이 Access-Control-Allow-Origin을 지원해야만 외부 모듈을 불러올 수 있습니다.-->
<!-- 그렇지 않으면 스크립트는 실행되지 않습니다.-->
<script type="module" src="http://another-site.com/their.js"></script>

 

- 상대 혹은 절대 경로가 반드시 필요하다

import {sayHi} from 'sayHi'; // Error!
// './sayHi.js'와 같이 경로 정보를 지정해 주어야 합니다.

 

- 구식브라우저는 module은 해석하지 못해서 스크립트를 무시해버린다

<script type="module">
  alert("모던 브라우저를 사용하고 계시군요.");
</script>

<script nomodule>
  alert("type=module을 해석할 수 있는 브라우저는 nomodule 타입의 스크립트는 넘어갑니다. 따라서 이 alert 문은 실행되지 않습니다.")
  alert("오래된 브라우저를 사용하고 있다면 type=module이 붙은 스크립트는 무시됩니다. 대신 이 alert 문이 실행됩니다.");
</script>

빌드 툴

하지만 이렇게 모듈스크립트를 html에 하나하나 직접 넣어서 사용하는 경우는 드물다.

대개 웹팩(webpack)고 같은 특별한 툴을 사용해서 여러개의 모듈을 하나의 파일로 번들링해서 개발한다.(여러개 가능)

 

번들러를 사용하면 경로가 없는 모듈이나, css, html 포맷도 모듈로 사용할 수 있다.

빌드 툴의 역할

- root가 되는 js 모듈을 선택한다.

- root 모듈을 기점으로 모듈간 의존성을 파악한다.

- 모듈 전체를 한데 모아 하나의 큰 파일을 만든다. (이 과정에서 import 문이 번들러 내함수로 대체된다)

- 이과정에서 변형과 최적화가 이루어진다.

* 도달 가능하지 않은 코드는 삭제

* 내보내진 모듈중 쓰임이 없는 모듈 삭제 (tree-shaking)

* console, debugger 같은 개발 관련 코드 삭제

* babel 적용

* 공백 제거 및 변수이름 줄이기로 번들링 파일 최소화 등

 

이렇게 번들링 된 파일은 import, export 가 특별한 번들러 함수로 대체되기 때문에 일반 스크립트처럼 취급할 수 있다

<!-- 웹팩과 같은 툴로 번들링 과정을 거친 스크립트인 bundle.js -->
<script src="bundle.js"></script>

모듈 다시 내보내기

export ... from .. 을 이용하면 모듈을 다시 내보낼 수 있다.

// 📁 auth/index.js
// login과 logout을 가지고 온 후 바로 내보냅니다.
export {login, logout} from './helpers.js';

// User 가져온 후 바로 내보냅니다.
export {default as User} from './user.js';
...
export * from './user.js'; // named export를 다시 내보내기
export {default} from './user.js'; // default export를 다시 내보내기

동적으로 import 하기

import() 표현식을 사용하면되는데,

모듈이 내보내는것을 담은 객체를 프로미스로 반환한다.

react 에서 lazyloading 을 할때 사용된다.

let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");

import(modulePath)
  .then(obj => <모듈 객체>)
  .catch(err => <로딩 에러, e.g. 해당하는 모듈이 없는 경우>)
  
//
let module = await import(modulePath)

 

또 다른 예시

let {hi, bye} = await import('./say.js');

hi();
bye();

 

* 동적 import는 일반 script에서도 동작한다. (module 이 아니어도됨)

* import 는 함수호출이 아니다. 따라서 call,apply가 불가능하다.

반응형

'Archive' 카테고리의 다른 글

JS 높이와 스크롤  (0) 2022.09.17
stompJS에 기여하기와 한단계 성장한 느낌  (0) 2022.09.16
제너레이터  (0) 2022.09.04
Promise의 5가지 API  (0) 2022.09.04
try ... catch 와 에러 핸들링  (0) 2022.09.04