ES Modules

JavaScript ES Modules(ESM)는 JavaScript 코드를 모듈 단위로 구성하고 관리할 수 있게 해주는 공식 표준 모듈 시스템이다.
이 시스템은 ECMAScript 2015(ES6)에서 처음 도입되었으며, 코드의 재사용성, 유지보수성, 그리고 의존성 관리를 크게 향상시켰다.

ES Modules는 JavaScript 생태계의 모듈화 표준으로 자리 잡았으며, 다음과 같은 큰 이점을 제공한다:

  1. 코드의 구조화와 모듈화 향상
  2. 의존성 관리 간소화
  3. 트리 쉐이킹과 코드 분할을 통한 성능 최적화
  4. 브라우저와 Node.js에서 일관된 모듈 시스템 사용 가능

웹 개발의 복잡성이 증가함에 따라 ES Modules는 유지보수 가능하고 확장 가능한 코드베이스를 구축하는 데 필수적인 도구가 되었다.
이제 대부분의 최신 JavaScript 프로젝트는 CommonJS에서 ES Modules로 이동하는 추세를 보이고 있다.

ES Modules의 핵심 특징

  1. 정적 구조
    ES Modules는 정적 구조를 가지고 있어 코드가 실행되기 전에 모듈 관계가 결정된다.
    이는 다음과 같은 이점을 제공한다:

    • 빌드 시점에서의 최적화 가능
    • 더 나은 트리 쉐이킹(사용하지 않는 코드 제거)
    • 정적 분석 도구의 효과적인 활용
  2. 명시적인 가져오기와 내보내기

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    // 내보내기 (export.js)
    export const greeting = "안녕하세요";
    export function sayHello(name) {
      return `${greeting}, ${name}님!`;
    }
    export default class Person {
      constructor(name) {
        this.name = name;
      }
    }
    
    // 가져오기 (import.js)
    import Person, { greeting, sayHello } from './export.js';
    // default export는 이름 변경 가능, named export는 원래 이름 사용
    
  3. 모듈 스코프
    각 모듈은 자체 스코프를 가지며, 명시적으로 내보내지 않은 변수, 함수, 클래스는 모듈 외부에서 접근할 수 없다.

ES Modules의 주요 문법

  1. 기본 내보내기와 가져오기
    모듈당 하나의 기본(default) 내보내기를 가질 수 있다:

    1
    2
    3
    4
    5
    6
    7
    8
    
    // math.js
    export default function add(a, b) {
      return a + b;
    }
    
    // app.js
    import add from './math.js'; // 원하는 이름으로 가져올 수 있음
    console.log(add(2, 3)); // 5
    
  2. 명명된 내보내기와 가져오기
    여러 값을 이름으로 내보내고 가져올 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // utils.js
    export function formatDate(date) {
      return date.toLocaleDateString();
    }
    export function formatCurrency(amount) {
      return `₩${amount.toLocaleString()}`;
    }
    
    // app.js
    import { formatDate, formatCurrency } from './utils.js';
    // 또는 별칭 사용
    import { formatDate as fmtDate, formatCurrency as fmtCurr } from './utils.js';
    
  3. 모듈 전체 가져오기
    모듈의 모든 내보내기를 네임스페이스 객체로 가져올 수 있다:

    1
    2
    3
    
    // app.js
    import * as Utils from './utils.js';
    console.log(Utils.formatDate(new Date())); // 2025. 3. 4.
    
  4. 동적 가져오기
    런타임에 조건부로 모듈을 로드할 수 있다:

    1
    2
    3
    4
    5
    6
    7
    
    // 필요할 때만 모듈 로드
    async function loadModule() {
      if (condition) {
        const { default: ModuleClass, helper } = await import('./heavy-module.js');
        // 이제 ModuleClass와 helper 사용 가능
      }
    }
    

브라우저에서의 ES Modules 사용

브라우저에서 ES Modules를 사용하려면 script 태그에 type="module" 속성을 추가한다:

1
2
3
4
5
6
7
<script type="module">
  import { sayHello } from './greetings.js';
  document.body.textContent = sayHello('방문자');
</script>

<!-- 또는 외부 파일로 -->
<script type="module" src="app.js"></script>

브라우저 특징

  1. 자동으로 strict 모드 적용
  2. 모듈별 스코프 (전역 변수 오염 방지)
  3. 단 한 번만 실행 (여러 번 가져와도)
  4. CORS 제한 (로컬 파일 시스템에서 직접 실행 불가능)
  5. 지연 로딩 (기본적으로 defer 속성처럼 작동)

Node.js에서의 ES Modules

Node.js에서 ES Modules를 사용하는 방법:

  1. .mjs 확장자 사용
  2. package.json"type": "module" 설정
  3. 항상 파일 확장자 포함 필요 (.js, .mjs)
1
2
3
4
5
6
7
// Node.js에서 ES Modules 사용 예
import fs from 'fs/promises';

async function readConfig() {
  const data = await fs.readFile('config.json', 'utf8');
  return JSON.parse(data);
}

ES Modules의 고급 기능

  1. 모듈 다시 내보내기
    다른 모듈의 기능을 자신의 API의 일부로 다시 내보낼 수 있다:

    1
    2
    3
    4
    5
    6
    7
    
    // re-export 예제
    export { default as User } from './models/user.js';
    export { default as Product } from './models/product.js';
    export { apiCall } from './utils/api.js';
    
    // 모두 다시 내보내기
    export * from './helpers.js';
    
  2. Top-level Await
    ES2022부터 모듈 최상위 레벨에서 await를 사용할 수 있다:

    1
    2
    3
    4
    5
    6
    7
    
    // database.js - 모듈 로드 시 데이터베이스 연결 대기
    import { MongoClient } from 'mongodb';
    
    const client = new MongoClient('mongodb://localhost:27017');
    await client.connect(); // 모듈이 로드될 때 실행
    
    export const db = client.db('myApp');
    

빌드 도구와의 통합

현대적인 JavaScript 개발에서는 빌드 도구와 함께 ES Modules를 사용한다:

  1. 웹팩(Webpack)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    // webpack.config.js 예제
    module.exports = {
      entry: './src/index.js',
      output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
      },
      // ESM을 지원하는 최신 JavaScript 처리
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          }
        ]
      }
    };
    
  2. Rollup
    트리 쉐이킹에 특히 효과적인 빌드 도구:

    1
    2
    3
    4
    5
    6
    7
    8
    
    // rollup.config.js 예제
    export default {
      input: 'src/main.js',
      output: {
        file: 'bundle.js',
        format: 'esm'  // ES 모듈 형식으로 출력
      }
    };
    

성능과 최적화

ES Modules의 성능 이점:

  1. 코드 분할: 필요한 모듈만 로드하여 초기 로딩 시간 단축
  2. 트리 쉐이킹: 사용하지 않는 코드 제거로 번들 크기 최적화
  3. 캐싱: 모듈 단위로 캐싱하여 재사용성 향상
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 코드 분할 예제
const AdminPanel = () => {
  return (
    <button onClick={async () => {
      // 사용자가 버튼을 클릭했을 때만 관리자 모듈 로드
      const { AdminDashboard } = await import('./admin.js');
      // 컴포넌트 렌더링
    }}>
      관리자 패널 열기
    </button>
  );
};

브라우저 호환성과 폴리필

ES Modules는 대부분의 현대 브라우저에서 지원되지만, 레거시 브라우저 지원이 필요한 경우 다음과 같은 접근 방식을 사용할 수 있다:

1
2
3
4
<!-- 현대 브라우저는 모듈 버전 사용 -->
<script type="module" src="app.esm.js"></script>
<!-- 레거시 브라우저는 번들 버전 사용 -->
<script nomodule src="app.bundle.js"></script>

참고 및 출처