자바스크립트 교재 · 19편 / 26편

모듈 — import·export·번들러

ESM vs CommonJS, default vs named, 번들러 한 줄.

중급읽는 시간 6분2026-05-17
여러 JS 파일이 import/export 로 연결된 모듈 그래프 그림

코드가 한 파일을 넘어가면 모듈이 필요합니다. JS 의 모듈은 역사상 두 가지가 공존 — ESM(현재 표준) 과 CommonJS(Node 의 옛 표준). 19편은 두 모양의 차이와, 그 위에 번들러가 무엇을 하는지 정리합니다.

ESM — 현재 표준

// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14159;

// 한꺼번에
export { add, PI };

// default — 모듈의 메인 export
export default function main() { ... }

// app.js
import main, { add, PI } from "./math.js";
//     ^ default      ^ named

import 모든 형태

import { a, b } from "./x.js";         // named
import { a as alias } from "./x.js";    // 별칭
import x from "./x.js";                 // default
import x, { a } from "./x.js";          // default + named
import * as X from "./x.js";            // 네임스페이스
import "./x.js";                        // side-effect (CSS·polyfill)

// 동적 import — 코드 분할
const m = await import("./x.js");
m.add(1, 2);

CommonJS — Node 의 옛 표준

// math.cjs
function add(a, b) { return a + b; }
const PI = 3.14159;
module.exports = { add, PI };

// 또는 default 비슷
module.exports = function main() { ... };

// app.cjs
const { add, PI } = require("./math.cjs");
const main = require("./math.cjs");

ESM vs CommonJS — 한 표

ESMCommonJS
문법import/exportrequire/module.exports
로딩정적 (트리 셰이킹 가능)동적 (런타임)
상위 await가능불가
브라우저표준 (script type="module")번들러 필요
파일 확장자.mjs 또는 type:module.cjs 또는 기본

package.json — type 필드

{
  "name": "my-pkg",
  "type": "module",       // ← 이 줄: .js 파일이 ESM 으로 해석됨
  "main": "./dist/index.js",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

2026년 새 프로젝트는 거의 "type": "module". CJS 와 호환이 필요한 라이브러리는 exports 로 두 가지를 모두 제공.

브라우저에서 ESM 직접 사용

<script type="module" src="./app.js"></script>

// app.js (브라우저)
import { greet } from "./greet.js";   // 확장자 필수
greet();

// CDN 으로 라이브러리 (esm.sh, unpkg)
import { z } from "https://esm.sh/zod@3";

브라우저 ESM 의 한계. ① 네트워크에 요청이 파일마다 따로 (작은 모듈 수십 개 → 느림). ② 확장자·경로 변환 없음. 실제 서비스는 거의 항상 번들러로 한 파일(또는 청크 몇 개) 로 묶음.

번들러 — esbuild·vite·webpack

# Vite — 2026년 표준
npm create vite@latest my-app
# → React/Vue/Svelte 등 템플릿 선택
# 개발: vite (dev server + HMR)
# 빌드: vite build → 최적화된 정적 파일

# esbuild — 단순 빌더
npm install -D esbuild
npx esbuild app.js --bundle --outfile=dist.js --minify

# webpack — 옛 표준 (가장 많은 옵션, 가장 복잡)
# tsup — 라이브러리 발행용 (esbuild 위)

2026년 선택지. ① 앱 — Vite (모든 프레임워크 호환, dev/build 빠름). ② 라이브러리 — tsup(esbuild 기반, ESM+CJS 동시 출력). ③ 모노레포·극도 커스터마이즈 — webpack/turbopack. webpack 은 새로 시작할 이유 거의 없음.

트리 셰이킹 — 안 쓰는 코드 제거

// util.js
export function used() { ... }
export function unused() { ... }   // 안 쓰는 함수

// app.js
import { used } from "./util.js";
used();

// 번들 결과 → unused 가 사라짐 (트리 셰이킹)
// ESM 만 가능 (CJS 는 동적이라 불가)

코드 분할 — dynamic import

// 무거운 라이브러리는 필요할 때만 로드
btn.addEventListener("click", async () => {
  const { default: Chart } = await import("./chart-lib.js");
  new Chart(...);
});

// React lazy
const Heavy = React.lazy(() => import("./Heavy.js"));

실전 — 모듈 디자인 가이드

  • 한 파일 한 책임. 100~300줄 정도가 보통 좋은 크기.
  • default 보다 named. 자동완성·검색·리팩터링 친화.
  • 외부에는 작은 API. index.js 에서 필요한 것만 re-export.
  • 순환 import 피하기. A → B → A 는 undefined 위험.
  • side-effect import 최소화. 트리 셰이킹을 위해 sideEffects: false(package.json).

순환 import 의 함정. A 가 B 를 import, B 가 A 를 import. ESM 은 부분 초기화 상태로 진행해서 undefined 가 들어갈 수 있음. 보통 한 쪽을 분리하거나 공통 모듈로 추출해 해결.

20편 — 정규식 기초

이메일·전화번호 검증, 그룹과 캡처.

📚 쉽게 배우는 자바스크립트 교재
이전: 18편 클래스 · 현재: 19편 (중급) · 다음 → 20편 정규식 · 진행: 19/26

© 2026 주나이테크(주) @JUNAITECH