ES 모듈 시스템
export와 import로 파일별 스코프와 공개 API를 구성하고 TypeScript 설정과 런타임에 따른 ES 모듈 해석을 이해합니다.
현대의 자바스크립트 및 타입스크립트 개발에서 코드를 조직하고 재사용하는 핵심적인 방법은 바로 모듈(Modules)을 사용하는 것입니다.
모듈은 코드를 여러 개의 독립적인 파일로 분리하고, 각 파일이 자신만의 스코프(Scope)를 가지도록 하여 전역 스코프 오염을 방지하며, 필요한 부분만 명시적으로 가져다 쓸 수 있게 해줍니다.
타입스크립트는 자바스크립트의 표준 모듈 시스템인 ES 모듈(ECMAScript Modules)을 지원합니다.
ES 모듈은 import와 export 키워드를 사용하여 모듈 간의 의존성을 관리하며, 실제 해석 방식은 module, moduleResolution, 런타임 설정에 따라 달라집니다.
export로 모듈 내보내기
TYPE / RUNTIME / CONTRACT
가져오는 파일에서 공개 API를 내보내는 파일로 읽는다
app.ts는 이름 있는 내보내기를 사용하고 main.ts는 기본 내보내기의 로컬 이름을 선택합니다. 화살표는 가져오는 파일 → 내보내는 파일입니다.
화살표는 사용하는 파일에서 내보내는 파일로 향하는 의존성입니다. named·별칭·default 연결은 원래 예제의 공개 API를 나타내며 함수 호출의 실행 순서 그림은 아닙니다.
NAMED
이름으로 가져온다
PI는 3.14159, add(5,3)은 8, Calculator(10).multiply(4)는 40입니다. 중괄호 안의 이름이 공개 API에 대응합니다.
ALIAS
가져올 이름과 로컬 이름
isValidEmail as checkEmail은 같은 함수에 로컬 이름을 붙입니다. checkEmail("[email protected]")은 true입니다.
DEFAULT
기본 값의 로컬 이름을 선택
UserService를 MyUserService로 가져옵니다. 생성한 service는 Alice를 addUser하고 getUsers로 저장한 목록을 반환합니다.
NAMESPACE
값과 타입의 존재를 구분
import * as Math로 Math.PI, Math.add, Math.Calculator를 사용합니다. Point와 ID는 타입 위치에서 참조하며 런타임 속성으로 생성되지 않습니다.
각 예제는 별도의 import 방식을 보여줍니다. 공개 API와 파일 스코프를 구분하고, 값 의존성의 순환이 생기면 초기화 순서도 확인하세요.
다른 파일에서 사용할 수 있도록 모듈의 변수, 함수, 클래스, 인터페이스, 타입 별칭 등을 공개하려면 export 키워드를 사용합니다.
각 선언 앞에 export 키워드를 붙여 개별적으로 내보냅니다.
여러 개를 내보낼 수 있으며, 가져올 때 내보낸 이름을 지정합니다. 로컬 이름은 as로 바꿀 수 있습니다.
export const PI = 3.14159; // 변수 내보내기
export function add(a: number, b: number): number { // 함수 내보내기
return a + b;
}
export class Calculator { // 클래스 내보내기
constructor(public value: number) {}
multiply(factor: number): number {
return this.value * factor;
}
}
export interface Point { // 인터페이스 내보내기
x: number;
y: number;
}
export type ID = string | number; // 타입 별칭 내보내기모듈의 끝에서 중괄호 {} 안에 내보낼 모든 것들을 나열하여 한꺼번에 내보낼 수도 있습니다.
function isValidEmail(email: string): boolean {
return email.includes('@') && email.includes('.');
}
function isValidPassword(password: string): boolean {
return password.length >= 8;
}
export { isValidEmail, isValidPassword };모듈에서 단 하나의 주된 값을 내보낼 때는 export default를 사용합니다.
export default는 모듈당 하나만 존재할 수 있으며, 가져올 때는 임의의 이름을 붙일 수 있습니다.
interface User {
id: number;
name: string;
}
class UserService {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
console.log(`User ${user.name} added.`);
}
getUsers(): User[] {
return this.users;
}
}
export default UserService; // UserService 클래스를 기본 내보내기기본 내보내기는 주로 한 모듈이 특정 클래스나 함수 하나를 대표할 때 사용됩니다.
공개한 값과 타입의 차이
위 파일에서 PI, add, Calculator는 실행할 때도 존재하는 값입니다. Point, ID는 타입 검사용 선언이므로 출력 JavaScript에는 이 이름의 값을 만들지 않습니다. 같은 파일에서 내보내도 가져오는 목적에 따라 import와 import type을 구별합니다. validation.ts의 이메일 검사는 @와 .의 포함 여부만 확인하는 학습용 조건이며 모든 실제 이메일 주소의 유효성을 판정하는 검증기는 아닙니다.
import로 모듈 가져오기
다른 모듈에서 내보낸 값을 사용하려면 import 키워드를 사용합니다.
중괄호 {} 안에 내보낸 이름을 그대로 사용하여 가져옵니다.
필요하다면 as 키워드를 사용하여 별칭을 지정할 수 있습니다.
import { PI, add, Calculator } from './mathUtils'; // 개별 내보내기 가져오기
import { isValidEmail as checkEmail } from './validation'; // 별칭 사용하여 가져오기
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
const calc = new Calculator(10);
console.log(calc.multiply(4)); // 40
console.log(checkEmail("[email protected]")); // true기본 내보내기로 내보낸 값을 가져올 때는 중괄호 없이 임의의 이름을 사용하여 가져옵니다.
import MyUserService from './userService'; // 기본 내보내기 가져오기 (임의의 이름 MyUserService)
const service = new MyUserService(); // MyUserService는 실제 UserService 클래스를 참조
service.addUser({ id: 1, name: "Alice" });
console.log(service.getUsers());모듈의 런타임 내보내기를 네임스페이스 객체로 가져올 수 있습니다. 인터페이스와 타입 별칭은 타입 위치에서만 사용할 수 있으며 런타임 객체의 속성은 아닙니다.
이는 주로 모듈의 멤버가 많거나, 이름 충돌을 피하고 싶을 때 사용됩니다.
import * as Math from './mathUtils'; // mathUtils.ts의 모든 멤버를 Math 객체로 가져오기
console.log(Math.PI);
console.log(Math.add(10, 20));
const newCalc = new Math.Calculator(20);
console.log(newCalc.multiply(5));특정 모듈이 내보내는 값은 없지만, 모듈을 가져오는 것만으로 전역 스코프에 영향을 주거나 특정 초기화 코드를 실행하는 경우에 사용됩니다.
// 이 파일은 직접 내보내는 것은 없지만, import만으로 특정 기능을 활성화할 수 있습니다.
// String.prototype.startsWith 폴리필 등...
console.log("Polyfills loaded!");import './polyfills'; // polyfills.ts 모듈을 가져와서 실행만 합니다.
console.log("Application started.");
// 출력:
// Polyfills loaded!
// Application started.모듈 경로 지정
import 문에서 모듈의 경로는 다음과 같이 지정됩니다. 아래 확장자 생략 예제는 이를 지원하는 번들러 등의 해석을 전제로 합니다. Node.js의 ESM 상대 경로는 실행 파일 확장자까지 지정해야 합니다.
- 상대 경로: 현재 파일 위치를 기준으로 다른 모듈의 경로를 지정합니다. (
./,../사용)import { someFunc } from './myFile'; import { anotherFunc } from '../components/AnotherComponent'; - 별칭 경로:
tsconfig.json의paths설정을 통해 특정 경로에 별칭을 부여하여 사용할 수 있습니다. 이 설정은 TypeScript의 해석을 돕지만 출력 import를 다시 쓰지는 않습니다. 같은 별칭을 실제 번들러나 런타임도 이해해야 합니다.tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@utils/*": ["src/utils/*"], "@models/*": ["src/models/*"] } } }app.ts // app.ts import { formatMoney } from '@utils/formatter'; // src/utils/formatter.ts 파일을 참조 import { User } from '@models/user'; // src/models/user.ts 파일을 참조 - 패키지 이름:
node_modules폴더에 설치된 npm 패키지를 가져올 때 사용합니다.import React from 'react'; import { useEffect } from 'react'; import axios from 'axios';
Node에서 직접 실행할 작은 구성
위의 app.ts 예시들은 서로 다른 가져오기 방식을 보여주므로 모두 한 파일에 붙이지 않습니다. 다음 실습은 앞의 mathUtils.ts 코드만 그대로 src/mathUtils.ts에 저장하고 별도 src/demo.ts를 만드는 구성입니다. TypeScript 컴파일러가 준비된 독립 폴더에서 진행합니다. package.json과 tsconfig.json을 폴더의 최상위에 둡니다.
{ "type": "module" }{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"verbatimModuleSyntax": true,
"types": []
},
"include": ["src/**/*.ts"]
}import { add, Calculator } from './mathUtils.js';
import * as mathValues from './mathUtils.js';
import type { Point, ID } from './mathUtils.js';
const point: Point = { x: 2, y: 3 };
const id: ID = 'P7';
console.log(add(point.x, point.y));
console.log(new Calculator(10).multiply(3));
console.log(Object.keys(mathValues).sort().join(','));
console.log(id);tsc -p .
node dist/demo.js출력은 순서대로 5, 30, Calculator,PI,add, P7 네 줄입니다. 소스는 .ts 파일이지만 상대 경로에는 실행할 .js 이름을 적습니다. 컴파일러는 대응하는 소스 파일을 검사하고, Node는 출력된 파일을 읽습니다. 출력 demo.js에서 타입 전용 가져오기와 Point, ID 타입 표기는 제거됩니다. 이것은 값인 id 변수까지 제거한다는 뜻은 아닙니다.
독립적으로 바꿔보기
point를 { x: 4, y: 6 }으로, 계산기의 시작 값과 곱할 값을 각각 5, 4로 바꾸면 처음 두 출력은 무엇일까요? mathValues.Point를 실행 값으로 읽을 수 있는지도 판단합니다. 파일 경로와 내보내기 선언은 그대로 둡니다.
내보내기와 계산 확인
합은 10, 곱은 20입니다. Point는 인터페이스이므로 런타임 네임스페이스 객체의 속성으로 사용할 수 없습니다. 에디터에서도 값 위치의 mathValues.Point는 진단됩니다. 값의 공개 API 세 개는 바꾸지 않았으므로 목록 출력은 그대로입니다.
앞의 baseUrl 설정 예제는 구형 프로젝트의 별칭 구성을 읽기 위한 예시입니다. TypeScript 6.0에서는 이 옵션에 폐기 예정 진단이 나므로 새 구성에서는 baseUrl 없이 paths의 각 경로에 ./src/…처럼 기준 경로를 적습니다. 이 설정도 실행 별칭을 만들지는 않으며, 실제 별칭의 추적 실습은 모듈 해석 전략에서 이어갑니다.
실행 환경에 따른 설정은 공식 모듈 참조, 구형 옵션의 전환은 TypeScript 6.0 설명에서 대조할 수 있습니다.
타입스크립트와 ES 모듈
타입스크립트는 .ts 파일에서 ES 모듈 문법을 사용하면, 컴파일 시 tsconfig.json의 module 설정에 따라 해당 모듈 코드를 자바스크립트의 ES 모듈(ESNext), CommonJS, AMD, UMD 등 다양한 모듈 형식으로 변환합니다.
AMD·UMD는 이전 TypeScript 프로젝트를 설명하는 출력 형식입니다. TypeScript 6.0에서는 해당 옵션에 폐기 예정 진단이 나므로, 현재 직접 실행 실습은 위의 NodeNext 구성을 사용합니다. 사용하는 컴파일러 버전과 실행 주체를 확인한 뒤 출력 형식을 선택합니다.
대부분의 현대 웹 개발 환경(React, Angular, Vue 등)에서는 번들러(Webpack, Vite, Rollup 등)와 함께 ES 모듈을 사용하는 것이 표준입니다.
타입스크립트는 이러한 환경에 맞춰 모듈 시스템을 유연하게 지원합니다.
ES 모듈 시스템은 코드의 모듈화와 재사용성을 극대화하고, 전역 스코프 오염을 방지하여 대규모 애플리케이션을 효율적으로 개발할 수 있게 해줍니다.
export와 import 키워드를 적절히 사용하여 코드를 기능별로 분리하고, 명확한 의존성 관계를 설정하는 것은 잘 조직된 타입스크립트 프로젝트의 기본입니다.
모듈을 나눌 때는 내보내기 방식만큼 타입 전용 import/export와 런타임 import의 경계를 분명히 하는 것이 중요합니다.
아래 흐름은 import 종류와 컴파일러·런타임의 해석 경계를 함께 확인합니다.
TYPE / RUNTIME / CONTRACT
가져온 값, 타입, 실행 효과를 구분한다
값을 쓰는 import와 타입 전용 import, 실행만 필요한 import는 역할이 다릅니다. TypeScript의 해석과 실제 JavaScript 로딩도 각각 확인합니다.
VALUE
가져온 값을 사용한다
app.ts는 PI, add, Calculator를 사용해 3.14159, 8, 40을 출력합니다. 이 코드에서 사용하는 값은 JavaScript의 연결에 남습니다.
TYPE ONLY · 보충
import type은 실행 연결에서 빠진다
Point와 ID는 타입 위치에서 사용합니다. import type 또는 export type으로 표시한 연결은 출력 JavaScript에서 제거됩니다.
SIDE EFFECT
바인딩 없이 모듈을 평가한다
import "./polyfills"는 모듈의 실행 효과를 가져옵니다. 본문에서는 "Polyfills loaded!"가 "Application started."보다 먼저 출력됩니다.
RESOLUTION
타입 검사와 실행 경로를 맞춘다
Node.js ESM의 상대 경로에는 실행 파일 확장자가 필요합니다. 본문의 확장자 생략은 이를 지원하는 해석 환경을 전제로 합니다. paths는 출력 import를 다시 쓰지 않습니다.
module과 moduleResolution은 실제 실행 환경에 맞춰 선택합니다. 일반 import의 제거 여부는 사용 방식과 컴파일 설정에도 영향을 받으므로 타입 전용 의도를 명시하세요.
프로젝트가 커질수록 import 문 하나가 실제 실행 코드인지, 타입 검사만을 위한 연결인지, 파일을 가져오는 순간 실행되는 부수 효과인지 구분하는 습관이 중요해집니다.