타입스크립트 교재 · 9편 / 20편

모듈 시스템 — ESM·CJS·.d.ts

import/export 문법, ESM vs CJS, paths·moduleResolution, .d.ts 한 줄.

기초읽는 시간 7분2026-05-17
여러 .ts 파일이 import/export 로 연결된 모듈 그래프 일러스트

코드가 한 파일을 넘어가는 순간 모듈 시스템과 마주칩니다. 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 — 한 표

ESMCommonJS
문법import/exportrequire / 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 typeRootstypes 옵션, 그리고 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

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