Frontend/JAVASCRIPT

현대 자바스크립트의 심장, ES Modules(ESM) 완벽 가이드: import와 export

미니임 2026. 3. 3. 23:17

 

웹 애플리케이션의 규모가 커지면서 코드의 양은 기하급수적으로 늘어났습니다. 과거에는 수천 줄의 코드를 하나의 파일에 담거나, 여러 파일을 <script> 태그로 순서에 맞춰 일일이 로드해야 했죠. 하지만 이는 전역 오염과 의존성 관리라는 지옥을 선사했습니다.

이 혼란을 잠재우기 위해 등장한 것이 바로 **ES Modules(ESM)**입니다. 이제 모듈 시스템은 단순히 파일을 나누는 도구를 넘어, 코드의 독립성을 보장하고 재사용성을 극대화하는 현대 개발의 필수 메커니즘이 되었습니다.


1. Deep Dive: 모듈 시스템의 작동 원리

모듈 시스템을 가장 쉽게 이해하는 비유는 **'레고 블록'**입니다. 완성된 성을 만들기 위해 우리는 각기 다른 모양의 블록(모듈)을 조립합니다. 이때 각 블록은 자신만의 독립된 공간을 가지며, 연결 부위(인터페이스)를 통해서만 다른 블록과 결합됩니다.

자바스크립트 엔진은 모듈을 로드할 때 다음의 3단계를 거칩니다:

  1. 구성 (Construction): URL을 통해 파일을 찾고, 소스 코드를 내려받아 모듈 레코드(Module Record)로 구문 분석합니다.
  2. 인스턴스화 (Instantiation): export된 값들을 담을 메모리 공간을 확보하고, import와 export가 해당 공간을 가리키도록 연결합니다. (이때 실제 코드는 실행되지 않습니다.)
  3. 평가 (Evaluation): 실제 코드를 실행하여 메모리 공간에 값을 채워 넣습니다.

2. Hands-on: 실전 비즈니스 로직 적용하기

단순한 더하기 예제 대신, 이커머스 시스템의 장바구니와 세금 계산 로직을 구현하며 import와 export의 실무적인 사용법을 알아보겠습니다.

(1) Named Export: 여러 기능을 내보낼 때

하나의 파일에서 여러 개의 변수나 함수를 공유해야 할 때 사용합니다. 내보낼 때 사용한 이름 그대로 가져와야 합니다.

JavaScript
 
// libs/taxCalculator.js

export const TAX_RATE = 0.1; // 10% 부가세율

// 특정 국가의 세금을 계산하는 로직
export function calculateTax(price, region = 'KR') {
  if (region === 'KR') {
    return price * TAX_RATE;
  }
  return price * 0.15; // 기타 지역 15%
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(amount);
}

(2) Default Export: 모듈의 대표 기능을 내보낼 때

파일당 단 한 번만 사용할 수 있으며, 가져올 때 이름을 자유롭게 지을 수 있습니다.

JavaScript
 
// services/CartService.js

export default class CartService {
  constructor() {
    this.items = [];
  }

  addItem(product) {
    this.items.push(product);
    console.log(`${product.name} 상품이 장바구니에 담겼습니다.`);
  }

  getTotalPrice() {
    return this.items.reduce((total, item) => total + item.price, 0);
  }
}

(3) 통찰력 있는 결합: 메인 로직에서 사용하기

JavaScript
 
// main.js
import CartService from './services/CartService.js'; // Default import (이름 변경 가능)
import { calculateTax, formatCurrency as format } from './libs/taxCalculator.js'; // Named import (as로 별칭 가능)

const myCart = new CartService();
myCart.addItem({ name: '고성능 키보드', price: 150000 });

const subTotal = myCart.getTotalPrice();
const tax = calculateTax(subTotal);
const total = subTotal + tax;

console.log(`소계: ${format(subTotal)}`);
console.log(`세금: ${format(tax)}`);
console.log(`최종 합계: ${format(total)}`);

3. 트러블슈팅(Troubleshooting): 흔히 겪는 실수들

  • 상대 경로의 명시: 브라우저 환경에서 ESM을 사용할 때는 반드시 확장자(.js)를 포함해야 합니다. (Node.js의 CommonJS와 가장 큰 차이점입니다.)
  • Live Bindings: import된 값은 읽기 전용(Read-only) 뷰입니다. 내보낸 쪽에서 값이 변하면 가져온 쪽에서도 반영되지만, 가져온 쪽에서 그 값을 직접 수정하려고 하면 TypeError가 발생합니다.
  • CORS 이슈: 로컬 파일 시스템(file://)에서 모듈을 열면 보안 정책상 오류가 발생합니다. 반드시 라이브 서버(Live Server) 환경에서 테스트해야 합니다.

4. Trade-offs: 모듈 시스템 선택의 고민

모듈화는 구조적으로 완벽해 보이지만, 고려해야 할 지점들이 있습니다.

장점 (Pros) 단점 및 고려사항 (Cons)
스코프 분리: 전역 변수 오염 방지 네트워크 오버헤드: 파일이 너무 잘게 쪼개지면 HTTP 요청 횟수가 증가 (HTTP/2에서는 완화)
의존성 파악: 코드 간의 관계가 명확해짐 런타임 호환성: 구형 브라우저(IE 등) 지원을 위해 Babel/Webpack 같은 빌드 도구 필수
Tree Shaking: 사용하지 않는 코드를 제거하여 번들 크기 최적화 용이 순환 참조: A가 B를 참조하고 B가 A를 참조할 때 발생하는 초기화 이슈 주의

5. 요약 및 제언

모듈 시스템은 단순히 코드를 나누는 기술이 아니라, 애플리케이션의 아키텍처를 설계하는 방식입니다. Named Export는 명확한 인터페이스를 제공할 때 유리하고, Default Export는 해당 모듈의 핵심 정체성을 드러낼 때 유용합니다.

복잡한 수식을 다루는 금융 엔진을 만든다면, 각 연산 로직을 모듈로 분리하여 관리해 보세요. 예를 들어, 복리 계산식인

$$A = P(1 + r/n)^{nt}$$

를 별도의 finance.js 모듈로 분리하면 검증과 재사용이 훨씬 수월해질 것입니다.

여러분의 프로젝트에서는 Default Export와 Named Export 중 어떤 것을 더 선호하시나요? 프로젝트 전체의 일관성을 위해 팀 내 컨벤션을 먼저 수립해 보는 것을 추천합니다.

반응형