
개발하는 애플리케이션의 크기가 커지면 언젠간 파일을 여러 개로 분리해야 하는 시점이 옵니다.
이때 분리된 파일 각각을 '모듈(module)'이라고 부르는데, 모듈은 대개 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성됩니다.
모듈은 단지 파일 하나에 불과합니다. 스크립트 하나는 모듈 하나입니다.
모듈을 불러오고 내보내는 등 자바스크립트의 모듈 시스템에는 현재 크게 두 가지 방식이 있습니다.
바로 CommonJS 방식과 ES Modules(ESM) 방식입니다.
CommonJS는 2009년 Node.js가 등장하면서 처음 본격적으로 도입된 모듈 시스템입니다.
브라우저에는 없던 모듈 기능이 필요했기 때문에, require()로 외부 모듈을 불러오고 module.exports로 내보내는 방식이 만들어졌습니다.
이 방식은 간단하고 직관적이며, Node.js의 기본 모듈 시스템으로 자리잡아 대부분의 npm 패키지가 이 구조를 따르고 있습니다.
하지만 동기적 로딩 방식이기 때문에, 브라우저나 비동기 환경에선 제약이 있었습니다.
ES Modules는 자바스크립트의 공식 표준 모듈 시스템으로,
2015년 ECMAScript 6 (ES6)에서 도입되었습니다.
import와 export 키워드를 사용하여 모듈을 정적으로 불러오고 내보내며,
정적 구조 기반이라 빌드 시 의존성 분석, 트리 쉐이킹, 코드 분할, top-level await 등이 가능합니다.
브라우저에서는 <script type="module">로 사용할 수 있고, 최신 Node.js에서는
package.json에 "type": "module"을 설정하면 사용할 수 있습니다.
CommonJS는 Node.js의 필요에 의해 먼저 등장한 현실적인 해결책이고,
ES Modules는 그 뒤를 잇는 공식 표준 모듈 시스템으로,
더 나은 성능과 최적화를 위해 설계되었습니다.
현재는 두 시스템이 공존하고 있지만, 점점 ES Modules가 기본이 되는 추세입니다.
새로운 프로젝트를 시작한다면 import / export 기반의 ESM을 사용하는 것을 권장합니다.
아래의 내용도 ESM을 기준으로 작성했습니다.
export는 파일이나 모듈 안의 함수, 변수, 객체, 클래스 등을 외부로 내보내는 데 사용됩니다.
이렇게 내보낸 값은 다른 파일에서 import를 통해 불러와 사용할 수 있습니다.
export에는 크게 두 가지 방법이 있습니다:
바로 Named Exports와 Default Exports입니다.
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// app.js
import { add, sub } from './math.js';
❗ 이름이 중요하므로, import 시 중괄호
{}를 사용해야 하고 이름이 정확히 일치해야 함
// logger.js
export default function log(msg) {
console.log("로그:", msg);
}
// app.js
import log from './logger.js';
❗ default export는 중괄호 없이 import하며, 원하는 이름으로 바꿔서 쓸 수 있음
// hybrid.js
export const A = 1;
export const B = 2;
export default function hello() {
console.log("Hello!");
}
// app.js
import hello, { A, B } from './hybrid.js';
지금까지 다룬 모듈은 모두 정적인 방식이었습니다.
정적 import는 다음과 같이, 모듈 이름을 코드 상에 미리 명시해야만 사용할 수 있습니다.
import something from './module.js';
그러나 우리는
import ... from getModuleName();
이런식으로 모듈에 함수 호출 결괏괎을 import 하고 싶을 수 있고
if(조건...){
import 모듈...;
}
이런식으로 조건부로 모듈을 import 하고 싶을 수 있습니다.
이럴 때 사용하는 것이 바로 import(module) 표현식입니다.
이 표현식은 모듈을 읽고, 모듈이 내보낸 것들을 포함한 객체를 담은 Promise를 반환합니다.
const module = await import('./module.js');
// 또는
import('./module.js').then((module) => {
// module 안에 export된 것들이 들어 있음
});
| 항목 | 내용 |
|---|---|
| 형태 | import('모듈경로') |
| 반환 | Promise 객체 |
| 구조 | 비동기 (await 또는 .then() 필요) |
| import 대상 | 모듈이 export한 객체 전체 |
| 사용 위치 | if문, 함수, 이벤트 핸들러 등 모든 위치에서 사용 가능 |
async function loadComponent(type) {
if (type === 'chart') {
const { draw } = await import('./chart.js');
draw();
} else {
const { render } = await import('./table.js');
render();
}
}
→ 조건에 따라 필요한 모듈만 선택적으로 불러올 수 있습니다.
const moduleName = './utils/' + getModuleName(); // ex: 'math.js'
const mod = await import(moduleName);
mod.sayHi();
→ getModuleName()의 결과에 따라 다른 모듈을 불러오는 완전한 동적 로딩도 가능합니다.
단, 번들러(예: Webpack)는 이 경우 정적 분석이 어려우므로 별도 설정이 필요할 수 있습니다.
| 상황 | 이유 |
|---|---|
| 초기 로딩 속도를 줄이고 싶을 때 | 필요한 순간에만 모듈을 불러오기 위해 |
| 조건에 따라 모듈이 달라질 때 | if 문 안에서 동적으로 처리 가능 |
| 이벤트가 발생했을 때만 필요할 때 | 버튼 클릭 등 사용자 행동 후 로딩 |
| 코드 분할이 필요할 때 | Webpack/Vite 등의 번들러가 자동 분할 처리 |
import()는 동적 모듈 로딩을 가능하게 해주는 ESM만의 유연한 기능입니다.await 또는 .then()을 사용해야 합니다.