...
자바스크립트 모듈
개발하는 애플리케이션의 크기가 커지면 언젠간 파일을 여러 개로 분리해야 하는 시점이 옵니다.
이때 분리된 파일 각각을 '모듈(module)'이라고 부르는데, 모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성됩니다.
모듈은 단지 파일 하나에 불과합니다. 스크립트 하나는 모듈 하나입니다.
모듈에 특수한 지시자 export와 import를 적용하면 다른 모듈을 불러와 불러온 모듈에 있는 함수를 호출하는 것과 같은 기능 공유가 가능합니다.
모듈 export
파일이나 모듈안의 함수나, 객체를 export 하는데 사용됩니다.
export에는 Named exports와 Default exports 두가지 방법이 있습니다.
// 변수, 함수 선언식을 하나씩 export
export let name1, name2, …, nameN; // var, const도 동일
export let name1 = …, name2 = …, …, nameN; // var, const도 동일
export function functionName(){...}
export class ClassName {...}
// 변수명, 함수명을 모아 멤버 목록으로 export
export { name1, name2, ..., nameN };
export { variable1 as name1, variable2 as name2, ..., nameN }; // 별칭으로 export
// 비구조화로 내보내기
export const { name1, name2: bar } = o;
// default export
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export 사용법
Named exports
Named exports는 여러값을 export 하는데 유용합니다.
export 된 이름을 사용하여 import 하여 사용할 수 있습니다.
export const arrs = [10, 20, 30, 40]; // 개별로 선언해서 export
export { arrs2, getName }; // 묶어서 export
const arrs2 = [100, 200, 300, 400];
function getName() {
return "aaaaaaaaa";
}
Default exports
모듈 당 딱 한 개의 default export만 있어야 합니다.
default export로 객체, 함수 클래스 등이 될 수 있습니다.
가장 간단하게 export 할 수 있으며, 딱 한개만 default export를 할 수 있기 때문에, "메인" 이라고 할 수 있는 것을 default export 하는 것이 좋습니다.
let cube = function cube(x) {
return x * x * x;
}
export default cube;
다시 내보내기 / 조합
부모 모듈이 자식 모듈을 가져와서 다시 내보낼 수도 있습니다.
즉, 여러 개의 모듈을 모아놓을 하나의 모듈을 만들 수 있습니다.
export foo from 'bar.js';
위 구문은 아래와 동일합니다.
import foo from 'bar.js';
export foo;
모듈 import
외부 스크립트 또는 외부 모듈의 export된 함수, 객체를 가져오는데 사용됩니다.
// named
import * as name from "module-name";
import name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name"; // member이름이 길 경우 as 별명 가능
import { member1, member2 } from "module-name";
import { member1, member2 as alias2, [...] } from "module-name";
// default
import defaultMember, { member [, [...]] } from "module-name";
import defaultMember, * as alias from "module-name";
import defaultMember from "module-name";
import "module-name";
/*
name : 가져온 값을 받을 객체 이름.
member, memberN : export 된 모듈에서 멤버의 이름
defaultMember : export 된 모듈의 default 이름
alias, aliasN : export된 멤버의 이름을 지정한 이름
module-name : 가져올 모듈 이름 (파일명)
*/
import 사용법
모듈 전체 가져오기
import * as myModule from "my-module.js";
// myModule.sayHello()
하나의 멤버 가져오기
import {myMember} from "my-module.js";
여러개의 멤버 가져오기
import {foo, bar} from"my-module.js";
다른 이름으로 멤버 가져오기
멤버를 가져올 때 다른 이름으로 멤버를 지정할 수있습니다.
export 된 멤버 이름이 길거나, 복잡할 때, 임의의 이름으로 멤버를 지정할 수 있습니다.
import {reallyReallyLongModuleMemberName as shortName} from "my-module.js";
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module.js";
바인딩 없이 모듈만 실행하기
단순히 특정 모듈을 불러와 실행만 할 목적이라면, import만 사용하는 것이 좋습니다.
import "my-module.js";
default export 값 가져오기
default export를 통해 export 된 값들을 가져올 수 있습니다.
이때 { } 는 넣지 않습니다.
import myModule from "my-module.js";
위에서 설명한 기본 구문과 함께 사용할 수도 있습니다.
이 때, 기본 값(default export 된 값)을 가져오는 부분이 먼저 선언되야 합니다.
import myDefault, * as myModule from "my-module.js";
// 또는
import myDefault, {foo, bar} from "my-module.js";
동적으로 import 하기
지금까지 다뤘던 export 문이나 import 문은 ‘정적인’ 방식입니다.
문법이 단순하고 제약사항이 있죠.
첫 번째 제약은 import문에 동적 매개변수를 사용할 수 없다는 것이었습니다.
모듈 경로엔 원시 문자열만 들어갈 수 있기 때문에 함수 호출 결괏값을 경로로 쓰는 것이 불가능했습니다.
import ... from getModuleName(); // 모듈 경로는 문자열만 허용되기 때문에 에러가 발생합니다.
두 번째 제약은 런타임이나 조건부로 모듈을 불러올 수 없다는 점이었습니다.
if(...) {
import ...; // 모듈을 조건부로 불러올 수 없으므로 에러 발생
}
{
import ...; // import 문은 블록 안에 올 수 없으므로 에러 발생
}
import(module)
import(module) 표현식은 모듈을 읽고 이 모듈이 내보내는 것들을 모두 포함하는 객체를 담은 이행된 프라미스를 반환합니다.
코드 내 어디에서 동적으로 사용할 수도 있습니다.
let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");
import(modulePath)
.then(obj => "<모듈 객체>")
.catch(err => "<로딩 에러, e.g. 해당하는 모듈이 없는 경우>");
// let module = await import(modulePath)
async 함수 안에서 다음 같이 사용하는 것도 가능합니다.
let module = await import(modulePath)
모듈 say.js를 이용해 예시를 만들어보겠습니다.
// 📁 say.js
export function hi() {
alert(`안녕하세요.`);
}
export function bye() {
alert(`안녕히 가세요.`);
}
아래와 같이 코드를 작성하면 모듈을 동적으로 불러올 수 있습니다.
let say = await import('./say.js');
say.hi();
say.bye();
let {hi, bye} = await import('./say.js');
hi();
bye();
say.js에 default export를 추가해보겠습니다.
// 📁 say.js
export default function() {
alert("export default한 모듈을 불러왔습니다!");
}
default export 한 모듈을 사용하려면 아래와 같이 모듈 객체의 default 프로퍼티를 사용하면 됩니다.
let obj = await import('./say.js');
let say = obj.default;
// 위 두 줄을 let {default: say} = await import('./say.js'); 같이 한 줄로 줄일 수 있습니다.
say();
참고
동적 import는 일반 스크립트에서도 동작합니다. script type="module"가 없어도 됩니다.
주의
import()는 함수 호출과 문법이 유사해 보이긴 하지만 함수 호출은 아닙니다.
super()처럼 괄호를 쓰는 특별한 문법 중 하나입니다.
따라서 import를 변수에 복사한다거나 call/apply를 사용하는 것이 불가능합니다. 함수가 아니기 때문이죠.
브라우저(HTML)에서 모듈 사용 하기
모듈은 특수한 키워드나 기능과 함께 사용되므로 <script type="module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야 합니다. (포스팅 맨 마지막에 브라우저 코드를 작성했습니다.)
<!DOCTYPE html>
<script type="module">
import {sayHi} from './say.js';
document.body.innerHTML = sayHi('John');
</script>
모듈은 로컬 파일에서 동작하지 않고, HTTP 또는 HTTPS 프로토콜을 통해서만 동작합니다.
로컬에서 file:// 프로토콜을 사용해 웹페이지를 열면 import, export 지시자가 동작하지 않습니다.
브라우저 모듈 특징
모듈 스크립트는 항상 지연 실행
외부 모듈 스크립트 <script type="module" src="...">를 다운로드할 때 브라우저의 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>
순서 : <script> -> DOM(button) -> <script type="module">
모듈 비동기 처리
async 속성이 붙은 스크립트 태그 로딩이 끝나면 다른 스크립트나 HTML 문서가 처리되길 기다리지 않고 바로 실행됩니다. 모듈 스크립트에선 async 속성을 인라인 스크립트에도 적용할 수 있습니다.
가져오기(./analytics.js) 작업이 끝나면 HTML 파싱이 끝나지 않았거나 다른 스크립트가 대기 상태에 있더라도 모듈이 바로 실행됩니다.
이런 특징은 광고나 문서 레벨 이벤트 리스너, 카운터 같이 어디에도 종속되지 않는 기능을 구현할 때 유용하게 사용할 수 있습니다.
<!-- 필요한 모듈(analytics.js)의 로드가 끝나면 -->
<!-- 문서나 다른 <script>가 로드되길 기다리지 않고 바로 실행됩니다.-->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
모듈은 반드시 경로 설정
import {sayHi} from 'sayHi'; // Error!
// './sayHi.js'와 같이 경로 정보를 지정해 주어야 합니다.
모듈은 단 한번만 평가됨
동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 단 한 번만 실행됩니다.
실행 후 결과는 이 모듈을 가져가려는 모든 모듈에 내보내 집니다.
alert 함수가 있는 모듈(alert.js)을 여러 모듈에서 가져오기로 해봅시다. 얼럿 창은 단 한 번만 나타납니다.
// 📁 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"; // 모듈에 이미 정해진 John이름을 바꿈
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// 1.js와 2.js 모두 같은 객체를 가져오므로
// 1.js에서 객체에 가한 조작을 2.js에서도 확인할 수 있습니다.
모듈은 단 한 번만 실행되고 실행된 모듈은 필요한 곳에 공유되므로 어느 한 모듈에서 admin 객체를 수정하면 다른 모듈에서도 변경사항을 확인할 수 있습니다.
브라우저 모듈 예제 코드
index.html
<body>
<!--<script type="module" src="./mod.js"></script> 모듈을 굳이 선언 안해도 된다.-->
<script type="module" src="./go.js"></script>
</body>
mod.js
// 모듈 js
export const arrs = [10, 20, 30, 40];
export { arrs2, getName };
const arrs2 = [100, 200, 300, 400];
function getName() {
return "aaaaaaaaa";
}
go.js
// 실행 js
import { arrs, arrs2, getName } from './mod.js';
console.log(arrs);
console.log(arrs2);
console.log(getName());
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.