모듈 시스템 — ESM·CJS·.d.ts
import/export 문법, ESM vs CJS, paths·moduleResolution, .d.ts 한 줄.
코드가 한 파일을 넘어가는 순간 모듈 시스템과 마주칩니다. JS 의 모듈은 역사상 두 가지가 공존해 왔어요 — CommonJS(CJS) 와 ESM(ES Modules). TS 가 둘 다 받쳐주지만, 2026년 새 코드는 거의 항상 ESM 입니다. 9편은 import/export 의 모든 변형과 tsconfig 의 모듈 옵션을 정리합니다.
ESM 기본 — export · import
// math.ts
export function add(a: number, b: number) { return a + b; }
export const PI = 3.14159;
export type Pair = [number, number];
// 한꺼번에 묶어서 export 도 가능
export { add, PI, type Pair };
// app.ts
import { add, PI, type Pair } from "./math";
// ^ 타입만 import 는 type 키워드 권장 (런타임 코드 0)
add(1, 2);
console.log(PI);
default vs named
// utils.ts — default export
export default function format(x: unknown): string {
return String(x);
}
// 이름을 호출자가 정함
import format from "./utils"; // 어떤 이름이든 가능
import fmt from "./utils"; // 같은 모듈
// named — 이름 고정
export function parse(...) { ... }
export function serialize(...) { ... }
import { parse, serialize } from "./utils";
import { parse as p } from "./utils"; // 별칭
default vs named — 컨벤션. 모듈에 단 하나의 주된 export 만 있으면 default(예: React 컴포넌트 파일). 여러 함수·타입·상수가 같이 있는 유틸 모듈은 named. 큰 코드베이스는 named-only 가 일관성 좋다는 의견이 많습니다(자동완성·검색 친화).
import 모든 변형
import { a, b } from "./x"; // named
import { a as alias } from "./x"; // 별칭
import x from "./x"; // default
import x, { a } from "./x"; // default + named
import * as X from "./x"; // 전체를 namespace 로
import "./x"; // side-effect 만 (CSS·polyfill 등)
import type { T } from "./x"; // 타입만 (런타임 0)
import { type T, fn } from "./x"; // 혼합 (5.0+)
// 동적 import — 코드 분할
const m = await import("./x");
m.fn();
ESM vs CommonJS — 한 표
| ESM | CommonJS | |
|---|---|---|
| 문법 | import/export | require / module.exports |
| 로딩 | 정적 분석 (트리 셰이킹 가능) | 동적 (런타임) |
| 상위 await | 가능 | 불가 |
| 파일 확장자 | .mjs 또는 package.json type:module | .cjs 또는 type:commonjs (기본) |
| 브라우저 | 표준 (script type="module") | 번들러 필요 |
| Node 호환 | 22+ 안정 | 레거시 광범위 |
같은 프로젝트에 둘이 섞이면 지옥. Node 에서 ESM 이 CJS 를 default import 하면 wrapping 됩니다 — import pkg from "cjs-pkg". esModuleInterop·allowSyntheticDefaultImports tsconfig 옵션이 이 호환을 정리해 줍니다. 새 프로젝트는 처음부터 ESM 으로.
tsconfig — module · moduleResolution
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext", // import/export 그대로 유지
"moduleResolution": "Bundler", // 최신 esbuild/vite 와 호환
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true, // 파일별 변환 도구 호환
"resolveJsonModule": true // import data from "./x.json"
}
}
2026년 표준 조합: module=ESNext + moduleResolution=Bundler. 이전엔 Node16·NodeNext 가 권장이었는데, 번들러 + 프레임워크 시대에는 Bundler 가 가장 무난.
paths — alias 로 깊은 경로 줄이기
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}
}
// 사용
import Header from "@components/Header"; // ../../../components/Header 대신
import { db } from "@/lib/db";
주의. paths 는 TS 컴파일러 + 일부 번들러에만 알려집니다. 런타임 Node 에서 그대로 쓰려면 tsconfig-paths·tsc-alias·번들러(esbuild/vite) 같은 도우미가 필요합니다. 모르면 처음에는 안 쓰는 게 단순.
.d.ts — 타입만 들어있는 파일
// types/global.d.ts
declare global {
interface Window {
myApp: { version: string };
}
}
// 외부 JS 라이브러리에 타입 입히기 (없을 때)
declare module "untyped-lib" {
export function doStuff(x: string): number;
}
// 모듈 없이 단순 타입 선언
export type ApiResponse<T> = { ok: boolean; data: T };
tsconfig typeRoots 와 types 옵션, 그리고 npm 의 @types/* 패키지(DefinitelyTyped) 가 큰 생태계입니다. 17편(namespace 와 .d.ts) 에서 더 깊게.
실전 한 줄 가이드
- 새 프로젝트는 ESM + module=ESNext + moduleResolution=Bundler.
- 유틸 모듈은 named export, 컴포넌트 파일은 default 1개가 흔함.
- 타입만 가져올 땐
import type— 런타임 코드 0. - 같은 프로젝트에 ESM·CJS 혼용 피하기. CJS 라이브러리 쓸 때만 esModuleInterop.
- paths 는 진짜 필요할 때만 — 런타임 호환성도 같이 챙겨야 함.
10편 — tsconfig 핵심 옵션 10가지
strict 가족, target/module, lib, include/exclude 의 실전 조합.
이전: 8편 클래스 · 현재: 9편 (기초) · 다음 → 10편 tsconfig · 진행: 9/20