
모듈
여기서는 모듈의 개념적인(혹은 단편적인) 부분만 다루고 동적인 import가 주 내용입니다. 모듈에 대한 🔗자세한 내용은 여기를 참고해주세요.
'모듈'이란?
Module을 쉽게 설명하자면 하나의 재사용 가능한 부품을 의미한다고 보시면 됩니다.
Module을 한 줄로 정의하자면, 파일 하나하나 특정 기능을 갖는 작은 코드 단위를 의미합니다.(분리된 파일 각각을 모듈이라고 부릅니다.) 즉, 프로그램을 구성하는 내부의 코드가 기능별로 나뉘어져있는 형태를 의미합니다.
호스트 환경(=자바스크립트가 구동되는 환경: 브라우저, nodejs 등)에 따라서 서로 다른 모듈화 방법이 제공되고 있습니다.
또한 한 번 다운로드된 모듈은 웹브라우저에 의해서 저장되기 때문에 동일한 로직을 로드할 때 시간과 네트워크 트래픽을 절약할 수 있습니다.
자바스크립트가 만들어진 지 얼마 안 되었을 때는 자바스크립트로 만든 스크립트의 크기도 작고 단순했기 때문에 자바스크립트는 긴 세월동안 모듈 관련 표준 문법 없이 성장했습니다. 새로운 문법을 만들 필요가 없었던 것이죠. 그런데 스크립트의 크기가 커지고 복잡해지면서 모듈 단위로 구성해주는 방법을 만드는 등 다양한 시도를 하게 됩니다.
자바스크립트가 발전하면서 ES2015+ 부터는 모듈 시스템이 표준으로 등재되었습니다.
- 라이브러리라는 개념도 많이 들어봤을 것입니다. 라이브러리는 모듈과 비슷한 개념이라고 할 수 있습니다.모듈이 프로그램을 구성하는 작은 부품으로서의 로직을 의미한다면, 라이브러리는 자주 사용되는 로직을 재사용하기 편리하도록 잘 정리한 일련의 코드들의 집합을 의미합니다.
객체를 내보내는 모듈의 예시를 보겠습니다.
모듈은 단 한 번만 실행되고 실행된 모듈은 필요한 곳에 공유되므로 어느 한 모듈에서 객체를 수정하면 다른 모듈에서도 변경사항을 확인할 수 있다는 특징이 있습니다.
// 📁 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(); // 보라님, 안녕하세요!
아래처럼 '한꺼번에 모든 걸 가져오는 방식’을 사용하면 코드가 짧아집니다. 그런데도 어떤 걸 가져올 땐 그 대상을 구체적으로 명시하는 게 좋습니다.
// 📁 say.js
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
// 📁 main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
// 📁 main.js
import {sayHi} from './say.js';
이렇게 하는 데는 몇 가지 이유가 있습니다.
- 웹팩(webpack)과 같은 모던 빌드 툴은 로딩 속도를 높이기 위해 모듈들을 한데 모으는 번들링과 최적화를 수행합니다. 이 과정에서 사용하지 않는 리소스가 삭제되기도 합니다. 현재로선 say.js의 수 많은 함수 중 단 하나만 필요하기 때문에, 이 함수만 가져와 보겠습니다. 빌드 툴은 실제 사용되는 함수가 무엇인지 파악해, 그렇지 않은 함수는 최종 번들링 결과물에 포함하지 않습니다. 이 과정에서 불필요한 코드가 제거되기 때문에 빌드 결과물의 크기가 작아집니다. 이런 최적화 과정은 '가지치기(tree-shaking)'라고 불립니다.
- 어떤 걸 가지고 올지 명시하면 이름을 간결하게 써줄 수 있습니다. say.sayHi()보다 sayHi()가 더 간결하네요.
- 어디서 어떤 게 쓰이는지 명확하기 때문에 코드 구조를 파악하기가 쉬워 리팩토링이나 유지보수에 도움이 됩니다.
동적인 import
위에서 다룬 export문이나 import문은 '정적인' 방식입니다.
문법이 단순하고 제약사항이 있죠.
첫 번째 제약은 import문에 동적 매개변수를 사용할 수 없다는 것이었습니다.
모듈 경로에는 원시 문자열만 들어갈 수 있기 때문에 함수 호출 결괏값을 경로로 쓰는 것이 불가능했습니다.
import ... from getModuleName(); // 모듈 경로는 문자열만 허용되기 때문에 에러가 발생합니다.
두 번째 제약은 런타임이나 조건부로 모듈을 불러올 수 없다는 점이었습니다.
if(...) {
import ...; // 모듈을 조건부로 불러올 수 없으므로 에러 발생
}
{
import ...; // import 문은 블록 안에 올 수 없으므로 에러 발생
}
import(module) 표현식
import(module) 표현식은 모듈을 읽고 이 모듈이 내보내는 것들을 모두 포함하는 객체를 담은 이행된 Promise를 반환합니다.
코드 내 어디에서든 동적으로 사용할 수 있습니다.
let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");
import(modulePath)
.then(obj => "<모듈 객체>")
.catch(err => "<로딩 에러, e.g. 해당하는 모듈이 없는 경우>");
// let module = await import(modulePath)
예시
// 📁 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'); // async 안에 없어도 await 가능
let say = obj.default;
// 위 두 줄을 let {default: say} = await import('./say.js'); 같이 한 줄로 줄일 수 있습니다.
say();
'await' expressions are only allowed within async functions and at the top levels of modules.
위와 같은 문구를 마주칠 수도 있습니다.
❓ `and at the top levels of modules.`
참고로 await 구문은 무조건 async 안에만 있어야 하는 것은 아닙니다.
- async 구문이 없더라도 module의 최상단에 위치한다면 async 없이 await을 사용하여 Promise를 resolve 할 수 있습니다.
- 함수 안에서만 async 구문이 필요합니다.
- 당연히 then메서드로도 Promise 해결이 가능합니다.
- 모듈이 되기 위해서는 import 혹은 export가 하나라도 있어야 합니다. 단, import(module) 표현식은 모듈이 되기 위해 필요한 import or export 조건에 해당하지 않습니다.
- esm 방식의 가져오기 구문을 사용하기 위한 모듈이 되기 위한 방법으로 확장자명을 `.mjs`로 변경하는 방법도 있습니다.
const boolean = true;
if (boolean) {
const {
arariyo: { age, name },
arirang,
} = await import('./cjs.js');
console.log(`${name} : ${age}`); // horang : 36
arirang(); // arirang
}
import, export 구문이 둘 다 없으면 모듈이 아니므로 await를 (module이 아니므로) 사용할 수 없습니다. import 할 것이 없을 때는 export 구문을 작성해서 모듈로 만드는 방법이 있습니다.
아래 코드에서 맨 위의 import 구문과 아래의 import 표현식은 다른 구문입니다.
import bye from "./say";
/**
* # await 의 조건
* ! 'await' expressions are only allowed within async functions and at the top levels of modules.
* 참고로 await 구문은 무조건 async 안에만 있어야 하는 것은 아니다. async 구문이 없더라도 module의 최상단에 위치한다면 await을 사용하여 Promise를 resolve 할 수 있다. 함수 안에서는 async 구문이 필요하다.
*/
const mod = await import("./say").then((mod) => mod.bye);
mod();
동적 import는 일반 스크립트에서도 동작합니다. script 태그의 type="module"이 없어도 됩니다.
🚨 주의
import() 표현식은 함수 호출과 문법이 유사해 보이긴 하지만 함수 호출은 아닙니다.
super()처럼 괄호를 쓰는 특별한 문법 중 하나입니다.
따라서 import를 변수에 복사한다거나 call/apply를 사용하는 것이 불가능합니다. 함수가 아니기 때문이죠.
활용 예시
예1) 조건식으로 불러올 수 있다.
import bye from "./say";
/**
* # await 의 조건
* ! 'await' expressions are only allowed within async functions and at the top levels of modules.
* 참고로 await 구문은 무조건 async 안에만 있어야 하는 것은 아니다. async 구문이 없더라도 module의 최상단에 위치한다면 await을 사용하여 Promise를 resolve 할 수 있다. 함수 안에서는 async 구문이 필요하다.
*/
const mod = await import("./say").then((mod) => mod.bye);
mod();
const Func = async () => {
const variables = 45;
if (variables > 50) {
const mod = await import("./say").then((mod) => mod.bye);
mod();
}
};
예2) `await` 키워드의 위치에 따라 다른 결과를 낼 수 있다.
await 키워드가 비동기적인 가져오기를 동기적으로 기다리도록 만들어줄 수 있는 범위는 함수 스코프입니다.
/**
*
* @returns 리턴되는 것은 동적 import에서 await을 쓰면서 Promise가 처리된 `number` 타입임을 보장함.
*/
async function test1() {
console.log(3);
const result = import('./ems2.mjs').then(dt => {
console.log('data transferred');
return dt.something;
});
console.log(4);
return await result;
}
/**
* async 함수의 호출결과를 기다리지는 않음.
*/
const result1 = test1();
console.log(result1); // Promise { <pending> }
// 🚀전체 로그 순서 정리
// 3
// 4
// Promise { <pending> }
// data transferred
위 예시에서는 test1 함수의 리턴문에서 await 키워드를 발견할 수 있습니다.
이렇게 되면 result1은 test1함수의 호출 결과를 Promise가 처리된 결과로 반환할까요?
그렇지 않습니다. test1함수 내부의 await 키워드는 test1함수 내부의 함수 스코프에서만 작동하기 때문에 test1함수를 호출했을 때는 Promise 그 상태로 result1 변수에 담기게 됩니다. 로그가 찍힌 순서를 살펴보겠습니다. 내부의 숫자를 찍는 로그들이 모듈 가져오기를 기다리지 않고 먼저 로그에 찍힙니다. 그 다음 Promise를 로그로 찍고, 마지막으로 then문의 microtask queue에 등록된 텍스트를 로그로 찍는 코드가 실행되어서 로그로 찍히는 모습을 볼 수 있습니다.('data transferred)
이번에는 await 키워드를 비동기적으로 모듈을 가져오는 import 표현식에 직접 붙여보겠습니다.
async function test1() {
console.log(3); // ---(1)
// --- (2)
const result = await import('./ems2.mjs').then(dt => { // --- (3)
console.log('data transferred');
return dt.something;
});
console.log(4); // --- (4)
return result;
}
const result1 = test1(); // --- (0)
console.log(result1);
// 🚀전체 로그 결과
// 3
// Promise { <pending> }
// data transferred
// 4
await이 import 표현식 바로 앞에 붙이고 return 문에서는 제거해줬습니다. test1 함수가 호출됐을 때 내부적으로는 `log(3)`이후에 import를 건너뛰지 않고 기다려준 후에 then문까지 실행되고 `log(4)`가 실행되는 모습을 볼 수 있습니다.
하지만, 여기서 result1에는 여전히 Promise가 pending상태로 처리가 되어서 담겨있네요. 이는 계속 말했듯이 await은 함수 스코프 한정이기 때문에 `result1` 변수에 Promise가 담길 뿐 fulfilled된 Promise가 담기지는 않았기 때문입니다.
마지막으로 함수 호출 부분, 즉, 함수 호출식에서 await을 해보겠습니다.
그 전에 함수 내부 import 표현식 앞의 await은 제거하겠습니다. 내부에서 비동기적으로 가져오는 모듈 데이터에 의존해야 하는 코드가 없기 때문에 굳이 기다릴 필요가 없기 때문입니다. `log(4)`가 먼저 찍히는 것 쯤이야 비동기적으로 가져오는 데이터와 의존성이 전혀 없는 코드이니 먼저 찍히도록 양보를 해줍시다.
async function test1() {
console.log(3);
const result = import('./ems2.mjs').then(dt => {
console.log('data transferred');
return dt.something;
});
console.log(4);
return result;
}
const result1 = await test1();
console.log(result1); // --- 234
// 🚀전체 로그
// 3
// 4
// data transferred
// 234
위 코드를 보면, Promise pending 상태의 로그가 찍히지 않았습니다. 함수 호출식에서 await 키워드를 붙여주면, 함수 내부에서 어떻게 돌아가든 간에 상관없이 호출한 함수의 모든 실행을 기다리라는 의미와 동일합니다. 그래서 Promise가 처리된 결과로 234라는 숫자가 result1에 담겨서 로그로 찍힌 것을 볼 수 있습니다.
바로 위 코드에서 조금은 부차적인 얘기지만, async 키워드를 test1 선언문 앞에서 지워도 동일한 결과를 냅니다. 호버했을 때 타입 추론도 동일하게 해주네요. 그렇기 때문에 '`async`는 의미가 없는 거 아닌가?' 하고 의문을 가질 수도 있습니다. 일단 코드에 미치는 영향은 없는 게 맞습니다. 하지만 관행상 내부에서 비동기적인 코드가 있는 경우 async를 붙여주는 것이 좋습니다. 그렇게 함으로써 함수가 조금 복잡해지더라도 이 함수는 내부에 비동기적인 요청이 들어가 있는 거구나 바로 알 수 있는 것이죠.
레퍼런스
https://overcome-the-limits.tistory.com/739
[자바스크립트] module.export, exports
들어가며 모듈을 만들고 활용하면서 module.export, exports에 대한 명확한 이해 없이 개념을 활용하고 있다는 것을 알았습니다. 이번 기회에 이 개념들을 명확하게 이해해야겠다고 생각했습니다. modu
overcome-the-limits.tistory.com
https://ko.javascript.info/modules
모듈
ko.javascript.info
[JS] 📚 모듈 사용하기 import / export 완벽 💯 정리
자바스크립트 모듈 개발하는 애플리케이션의 크기가 커지면 언젠간 파일을 여러 개로 분리해야 하는 시점이 옵니다. 이때 분리된 파일 각각을 '모듈(module)'이라고 부르는데, 모듈은 대개 클래스
inpa.tistory.com