front-end

모듈(module) 이란?

정구 2022. 2. 25. 18:30
728x90

프로그램은 작고 단순한 것에서 크고 복잡한 것으로 진화한다.

 

그 과정에서 코드의 재활용성을 높히고, 유지보수를 쉽게 하기 위해 개발하는 애플리케이션의 크기가 커지면 언젠간 파일을 여러 개로 분리해야 하는 시점이 온다.

 

이 때 분리된 파일을 각각 '모듈(module)' 이라고 부르는데, 모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성된다.

 

대부분의 주요 브라우저와 Node.js가 모듈 시스템을 지원하고 있다.

 

 

 

 

모듈이란?

모듈은 단지 하나의 파일에 불과하다. 스크립트 하나는 모듈 하나이다.

 

다음과 같은 특징을 가진다.

 

  • 자주 사용되는 코드를 별도의 파일로 만들어서 필요할 때마다 재활용할 수 있다.
  • 코드를 개선하면 이를 사용하고 있는 모든 애플리케이션의 동작이 개선된다.
  • 코드 수정 시에 필요한 로직을 빠르게 찾을 수 있다.
  • 필요한 로직만을 로드해서 메모리의 낭비를 줄일 수 있다.
  • 한 번 다운로드된 모듈은 웹 브라우제어 의해서 저장되기 때문에 동일한 로직을 로드할 때 시간가 네트워크 트래픽을 절약할 수 있다. (브라우저에서만 해당)

 

 

모듈에 특수한 지사자 exportimport를 적용하면 다른 모듈을 불러와 불러온 모듈에 있는 함수를 호출하는 것과 같은 기능 공유가 가능하다.

 

  • export : 지시자를 변수나 함수 앞에 붙이면 외부 모듈에서 해당 변수나 함수에 접근할 수 있다.(모듈 내보내기)
  • import : 지시자를 사용하면 외부 모듈의 기능을 가져올 수 있다.(모듈 가져오기)
// 📁 sayHi.js
export function sayHi(user) {
  alert(`Hello, ${user}!`);
}
// 📁 main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // 함수
sayHi('John'); // Hello, John!

 

위 예시에서 import 지시자는 상대 경로(./sayHi.js) 기준으로 모듈을 가져오고 sayHi.js에서 내보낸 함수 sayHi를 상응하는 변수에 할당한다.

 

 

브라우저에서 모듈이 어떻게 동작하는지 예시를 이용해 알아보자.

 

모듈은 특수한 키워드나 기능과 함께 사용되므로 <script type="module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야한다.

// 📁 index.html
<!doctype html>
<script type="module">
  import {sayHi} from './say.js';

  document.body.innerHTML = sayHi('John');
</script>
🚨 모듈은 로컬 파일에서 동작하지 않고, HTTP 또는 HTTPS 프로토콜을 통해서만 동작한다.
로컬에서 웹 페이지를 열면 import, export 지시자가 동작하지 않는다.
로컬 웹 서버인 static-server나, 코드 에디터의 Live Server Extension 등을 사용해야한다.

 

 

 

모듈의 핵심 기능

일반 스크립트와 모듈의 차이는 무엇일까?

 

 

엄격 모드로 실행됨

 

모듈은 항상 엄격 모드(use strict)로 실행된다. 선언되지 않은 변수에 값을 할당하는 등의 코드는 에러를 발생시킨다.

<script type="module">
  a = 5; // 에러
</script>

 

 

모듈 레벨 스코프

 

모듈은 자신만의 스코프가 있다. 따라서 모듈 내부에서 정의한 변수나 함수는 다른 스크립트에서 접근할 수 없다.

 

다음 예제를 살펴보자.

 

user.jshello.js를 가져오고 user.js에서 선언한 변수 userhello.js에서 사용해보자. 에러가 난 것을 확인할 수 있다.

// index.html
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>
//user.js
let user = "John";
//hello.js
alert(user);
// 모듈은 변수를 공유하지 않기 때문에 
//`Uncaught ReferenceError: user is not defined`라는 에러가 콘솔 패널에 출력됩니다.

 

외부에 공개하려는 모듈은 export 해야 하고, 내보내진 모듈을 가져와 사용하려면 import 해줘야한다.

 

전역변수를 대신하여 hello.jsuser.js를 가져와 필요한 기능을 얻을 수 있다.

//index.html
<!doctype html>
<script type="module" src="hello.js"></script>
//user.js
export let user = "John";
//hello.js
import {user} from './user.js';

document.body.innerHTML = user; // John

 

브라우저 환경에서도 <script type="module">을 사용해 모듈을 만들면 독립적인 스코프가 만들어진다.

<script type="module">
  // user는 해당 모듈 안에서만 접근 가능합니다.
  let user = "John";
</script>

<script type="module">
  alert(user); // Error: user is not defined
</script>

 

 

 

단 한 번만 평가됨

 

동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 단 한 번만 실행된다. 실행 후 결과는 이 모듈을 가져가려는 모든 모듈에 내보내 진다.

// 📁 alert.js
alert("모듈이 평가되었습니다!");
// 동일한 모듈을 여러 모듈에서 가져오기

// 📁 1.js
import `./alert.js`; // 얼럿창에 '모듈이 평가되었습니다!'가 출력됩니다.

// 📁 2.js
import `./alert.js`; // 아무 일도 발생하지 않습니다.

 

실무에선 최상위 레벨 모듈을 대개 초기화나 내부에서 쓰이는 데이터 구조를 만들고 이를 내보내 재사용하고 싶을 때 사용한다.

 

 

객채를 내보내는 모듈을 살펴보자.

// 📁 admin.js
export let admin = {
  name: "John"
};

이 모듈을 가져오는 모듈이 여러 개이더라도 모듈은 최초 호출 시 단 한 번만 평가된다.

 

이 때 admin 객체가 만들어지고 이 모듈을 가져오는 모든 모듈에 admin 객체가 전달된다. 각 모듈에 동일한 admin 객체가 전달되는 것이다.

// 📁 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에서도 확인할 수 있습니다.

다시 한번 강조하자면, 모듈은 단 한 번만 실행되고 실행된 모듈은 필요한 곳에 공유되므로 어느 한 모듈에서 admin 객체를 수정하면 다른 모듈에서도 변경사항을 확인할 수 있다.

 

이런 특징을 이용하면 모듈 설정(configuration)을 쉽게 할 수 있다.

 

최초로 실행되는 모듈의 객체 프로퍼티를 원하는 대로 설정하면 다른 모듈에서 이 설정을 그대로 사용할 수 있기 때문이다.

 

예시를 통해 살펴보자. admin.js 모듈은 어떤 특정한 기능을 제공해주는데, 이 기능을 사용하려면 외부에서 admin 객체와 관련된 인증 정보를 받아와야한다고 가정해보자.

// 📁 admin.js
export let admin = { };

export function sayHi() {
  alert(`${admin.name}님, 안녕하세요!`);
}
// 📁 init.js
import {admin} from './admin.js';
admin.name = "사용자";
// 📁 other.js
import {admin, sayHi} from './admin.js';

alert(admin.name); // 사용자

sayHi(); // 사용자님, 안녕하세요!

최초로 실행되는 스크립트인 init.js에서 admin.name을 설정해주면 admin.js를 포함하는 외부 스크립트에서 admin.name에 저장된 정보를 볼 수 있다.

 

 

 

import.meta

 

import.meta 객체는 현재 모듈에 대한 정보를 제공해준다.

 

호스트 환경에 따라 제공하는 정보의 내용은 다른데, 브라우저환경에선 스크립트의 URL 정보를 얻을 수 있다. 

 

HTML 안에 있는 모듈이라면, 현재 실행 중인 웹페이지의 URL 정보를 얻을 수 있다.

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

 

 

 

this. undefined

 

모듈의 최상위 레벨의 thisundefined이다.

 

모듈이 아닌 일반 스크립트의 this는 전역 객체인 것과 대조된다.

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

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

 

 

 

브라우저 특정 기능

브라우저 환경에서 type="module"이 붙은 스크립트가 일반 스크립트와 어떤 점이 다른지알아보자.

 

 

지연 실행

 

모듈 스크립트는 항상 지연 실행된다. 외부 스크립트, 인라인 스크립트와 관계없이 마치 defer 속성을 붙인 것 처럼 실행된다.

 

  • 외부 모듈 스크립트 <script type="module" src="...">를 다운로드 할 때 브라우저의 HTML 처리가 멈추지 않는다. 브라우저는 외부 모듈 스크립트와 기타 리소스를 병렬적으로 불러온다.
  • 모듈 스크립트는 HTML 문서는 완전히 준비될 때까지 대기 상태에 있다가 HTML 문서가 완전히 만들어진 이후에 실행된다. 모듈의 크기가 아주 작아서 HTML보다 빨리 불러온 경우도 마찬가지이다.
  • 스크립트의 상대적 순서가 유지된다. 문서상 위쪽의 스크립트부터 차례로 실행된다.

이런 특징 때문에 모듈 스크립트는 항상 완전한 HTML 페이지를 '볼 수' 있고 문서 내 요소에도 접근할 수 있다.

<script type="module">
  alert(typeof button);
  // 모듈 스크립트는 지연 실행되기 때문에 페이지가 모두 로드되고 난 다음에 alert 함수가 실행
  // 얼럿창에 object가 정상적으로 출력. 모듈 스크립트는 아래쪽의 button 요소를 '볼 수' 있다.
</script>


<script>
  alert(typeof button);
  // 일반 스크립트는 페이지가 완전히 구성되기 전이라도 바로 실행.
  // 버튼 요소가 페이지에 만들어지기 전에 접근하였기 때문에 undefined가 출력.
</script>

<button id="button">Button</button>

일반 스크립트는 모듈 스크립트보다 먼저 실행된다. 따라서 undefined가 먼저, object가 나중에 출력된다.

 

모듈 스크립트는 지연 실행되기 때문에 문서 전체가 처리되기 전까지 실행되지 않고, 일반 스크립트는 바로 실행되므로 위와 같은 결과가 나타난다.

 

페이지 내 특정 기능이 모듈 스크립트에 의존적인 경우, 모듈이 완전히 로딩되기 전에 페이지가 먼저 사용자에게 노출되기때문에 유의해야한다.

 

 

 

인라인 스크립트의 비동기 처리

 

모듈이 아닌 일반 스크립트에서 async 속성은 외부 스크립트를 불러올 때만 유효하다.

 

async 속성이 붙은 스크립트는 로딩이 끝나면 다른 스크립트나 HTML 문서가 처리되길 기다리지 않고 바로 실행된다.

 

반면, 모듈 스크립트에선 async 속성을 인라인 스크립트에도 적용할 수 있다. 그러면 다른 스크립트나 HTML이 처리되길 기다리지 않고 바로 실행된다.

 

import 작업이 끝나면 HTML 파싱이 끝나지 않았거나 다른 스크립트가 대기 상태에 있더라도 모듈이 바로 실행된다.

 

이런 특징은 광고나 문서 레벨 이벤트 리스너, 카운터 같이 어디에도 종속되지 않는 기능을 구현할 때 유용하게 사용할 수 있다.

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

  counter.count();
</script>

 

 

 

외부 스크립트

 

type="module" 가 붙은 외부 모듈 스크립트엔 두 가지 큰 특징이 있다.

 

1. src 속성값이 동일한 외부 스크립트는 한 번만 실행된다.

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

 

2. 외부 사이트같이 다른 오리진에서 모듈 스크립트를 불러오려면 CORS 헤더가 필요하다.

 

모듈이 저장되어있는 원격 서버가 Access-Control-Allow-Origin: * 헤더를 제공해야한 외부 모듈을 불러올 수 있다.

 

참고로 * 대신 페치(fetch)를 허용할 도메인을 명시할 수도 있다. 이 특징은 보안을 강화해준다.

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

 

 

 

경로가 없는 모듈은 금지

 

브라우저 환경에서 import는 반드시 상대 혹은 절대 URL 앞에 와야한다. '경로가 없는' 모듈은 허용되지 않는다.

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

 

 

 

호환을 위한 nomodule

 

구식 브라우저는 type="module"을 해석하지 못하기 때문에 모듈 타입의 스크립트를 만나면 이를 무시하고 넘어간다.

 

nomodule 속성을 사용하면 이런 상황을 대비할 수 있다.

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

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

 

 

 

빌드 툴

브라우저 환경에서 모듈을 '단독'으로 사용하는 경우는 흔치 않다.

 

대개 웹팩(Webpack)과 같은 특별한 툴을 사용해 모듈을 한 데 묶어(번들링) 프로덕션 서버에 올리는 방식을 사용한다.

 

번들러를 사용하면 무듈 분해를 통제할 수 있다. 여기에 더하여 경로가 없는 모듈이나 CSS, HTML 포맷의 모듈을 사용할 수 있게 해준다는 장점이 있다.

 

빌드 툴의 역할은 다음과 같다.

 

1. HTML의 <script type="module">에 넣을 '주요(main)' 모듈('진입점' 역할을 하는 모듈)을 선택한다.

2. '주요' 모듈에 의존하고 있는 모듈 분석을 시작으로 모듈 간의 의존 관계를 파악한다.

3. 모듈 전체를 한데 모아 한의 큰 파일을 만든다(설정에 따라 여러 개의 파일을 만드는 것도 가능하다). 이 과정에서 import 문이 번들러 내 함수로 대체되므로 기존 기능은 그대로 유지된다.

4. 이런 과정 중에 변형이나 최적화도 함께 수행된다.

  • 도달 가능하지 않는 코드는 삭제된다.
  • 내보내진 모듈 중 쓰임처가 없는 모듈을 삭제한다(가지치기 tree-shaking)
  • console, debugger 같은 개발 관련 코드를 삭제한다.
  • 최신 자바스크립트 문법이 사용된 경우 바벨(Babel)을 사용해 동일한 기능을 하는 낮은 버전의 스크립트로 변환한다.
  • 공백 제거, 변수 이름 줄이기 등으로 산출물의 크기를 줄인다.

 

번들링 툴을 사용하면 스크립트들은 하나 혹은 여러 개의 파일로 번들링된다.

 

이 때 번들링 전 스크립트에 있던 import, export 문은 특별한 번들러 함수로 대체된다.

 

번들링 과정이 끝나면 기존 스크립트에서 import, export가 사라지기 때문에 type="module"이 필요 없어진다.

 

따라서 아래와 같이 번들링 과정을 거친 스크립트는 일반 스크립트처럼 취급할 수 있다.

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

 

 

 

😎요약

1. 모듈은 하나의 파일이다. 브라우저에서 import, export 지시자를 사용하려면 <script type="module"> 같은 속성이 필요하다. 모듈은 아래와 같은 특징을 지닌다.

  • 지연 실행된다.
  • 인라인 모듈 스클비트도 비동기 처리 할 수 있다.
  • 외부 오리진(도메인이나 프로토콜, 포트가 다른 오리진)에서 스크립트를 불러오려면 CORS 헤더가 있어야한다.
  • 중복된 외부 스크립트는 무시된다.

 

2. 모듈은 자신만의 스코프를 갖는다. 모듈 간 기능 공유는 import, export로 할 수 있다.

 

3. 항상 엄격모드로 실행(use strict)된다.

 

4. 모듈 내 코드는 단 한 번만 실행된다. 모듈을 내보내려면 이 모듈을 가져오기 하는 모듈 모두가 내보내진 모듈을 공유한다.

 

 

 

 

728x90