Ch 01. ES 모듈과 import/export
TypeScript는 JavaScript의 ES 모듈 시스템을 기반으로 합니다. 파일 단위로 코드를 분리하고 필요한 것만 내보내고 가져오는 방식은 유지보수성과 번들 크기 최적화 모두에 영향을 줍니다.
named export와 default export
// 파일: src/modules/user.ts// named export — 여러 개 내보낼 수 있음export interface User { id: number; name: string; email: string;}export function createUser(name: string, email: string): User { return { id: Date.now(), name, email };}export const USER_ROLES = ["admin", "editor", "viewer"] as const;export type UserRole = (typeof USER_ROLES)[number];// default export — 파일당 하나만export default class UserService { private users: User[] = []; add(user: User): void { this.users.push(user); } findById(id: number): User | undefined { return this.users.find((u) => u.id === id); }}
// 파일: src/modules/app.ts// named importimport { User, createUser, USER_ROLES, UserRole } from "./user";// default import — 이름은 자유롭게 지정 가능import UserService from "./user";// 둘 다 함께import UserService2, { createUser as makeUser } from "./user";const service = new UserService();const alice: User = createUser("Alice", "[email protected]");service.add(alice);console.log(USER_ROLES); // ["admin", "editor", "viewer"]
named export가 default export보다 권장되는 이유는 두 가지입니다. import 시 이름 자동 완성이 동작하고, 이름을 바꿀 때 IDE 리팩터링 도구가 추적할 수 있습니다.
타입만 import/export하기
TypeScript 3.8부터 타입 전용 import/export 문법이 생겼습니다. 런타임에 존재하지 않는 타입을 명시적으로 다룰 때 유용합니다.
// 파일: src/modules/types.tsexport type { User } from "./user";export type { Product } from "./product";// 또는 한 파일에서export type UserRole = "admin" | "editor" | "viewer";export type ProductCategory = "electronics" | "books" | "clothing";
// 파일: src/modules/consumer.ts// 타입만 가져올 때 명시 — 번들러가 최적화할 수 있음import type { User } from "./user";import type { Product } from "./product";// 일반 값과 타입을 함께 가져올 때import { createUser, type User as UserType } from "./user";
re-export 패턴
다른 모듈에서 가져온 것을 다시 내보낼 때 사용합니다.
// 파일: src/modules/index.ts// 전체 re-exportexport * from "./user";export * from "./product";// 선택적 re-exportexport { createUser, USER_ROLES } from "./user";export { createProduct } from "./product";// 이름 변경 후 re-exportexport { UserService as default } from "./user";// 타입만 re-exportexport type { User } from "./user";export type { Product } from "./product";
배럴 파일(Barrel File) 패턴
배럴 파일은 디렉터리의 진입점 역할을 하는 index.ts 파일입니다. 여러 파일을 하나의 진입점으로 통합합니다.
src/
models/
user.ts
product.ts
order.ts
index.ts ← 배럴 파일
services/
user-service.ts
product-service.ts
index.ts ← 배럴 파일
// 파일: src/models/index.tsexport * from "./user";export * from "./product";export * from "./order";
// 파일: src/main.ts// 배럴 파일 없이import { User } from "./models/user";import { Product } from "./models/product";import { Order } from "./models/order";// 배럴 파일 사용import { User, Product, Order } from "./models";
배럴 파일의 주의점은 tree-shaking입니다. 배럴 파일에서 너무 많은 것을 re-export하면 번들러가 사용하지 않는 코드를 제거하기 어려울 수 있습니다. 대규모 라이브러리를 만들 때는 각 모듈을 직접 import하는 방식도 고려합니다.
동적 import
코드 분할(code splitting)이나 조건적 모듈 로딩에 사용합니다.
// 파일: src/modules/dynamic.tsasync function loadModule() { // 동적 import는 Promise를 반환 const { createUser } = await import("./user"); const user = createUser("Bob", "[email protected]"); return user;}// 조건적 로딩async function loadPlugin(name: string) { if (name === "chart") { const module = await import("./plugins/chart"); return module.default; } if (name === "table") { const module = await import("./plugins/table"); return module.default; } throw new Error(`알 수 없는 플러그인: ${name}`);}
import 경로 별칭 설정
tsconfig.json의 paths 옵션으로 상대 경로 대신 별칭을 사용할 수 있습니다.
// tsconfig.json{ "compilerOptions": { "baseUrl": ".", "paths": { "@models/*": ["src/models/*"], "@services/*": ["src/services/*"], "@utils/*": ["src/utils/*"] } }}
// 파일: src/api/users.ts// 상대 경로import { User } from "../../models/user";import { UserService } from "../../services/user-service";// 별칭 사용 (훨씬 명확하고 이동해도 경로 불변)import { User } from "@models/user";import { UserService } from "@services/user-service";
ES 모듈 시스템을 올바르게 활용하면 코드를 명확하게 구조화하고, 의존성을 명시적으로 관리하고, 번들 크기를 최적화할 수 있습니다. 배럴 파일로 공개 API를 정리하고, 타입 전용 import로 런타임 영향을 줄이는 것이 실전 TypeScript 프로젝트의 기본 관행입니다.