JavaScript 2026.02.11 📖 12분 분량

ES6 모듈 시스템으로 코드 모듈화하기

import/export를 활용한 체계적인 JavaScript 코드 구성 방법

모듈화가 필요한 이유

프로젝트를 시작할 때 모든 JavaScript 코드를 하나의 파일에 작성하면 간단해 보입니다. 하지만 코드가 500줄, 1000줄로 늘어나면 어디에 무엇이 있는지 찾기 어려워집니다. ES6 모듈 시스템은 이 문제를 해결해줍니다.

모듈화의 장점

  • 코드 구조화: 관련된 코드를 하나의 파일로 묶어 관리합니다
  • 네임스페이스 관리: 전역 변수 충돌을 방지합니다
  • 의존성 명시: 어떤 모듈이 어떤 모듈을 사용하는지 명확합니다
  • 재사용성: 다른 프로젝트에서도 모듈을 쉽게 재사용할 수 있습니다
  • 지연 로딩: 필요한 모듈만 로드하여 성능을 개선할 수 있습니다

기본 문법: export와 import

Named Export

여러 개의 값을 내보낼 때 사용합니다. 저는 TestModel 클래스를 export 할 때 이 방식을 사용했습니다.

// models/TestModel.js
export class TestModel {
    constructor() {
        this.tests = [];
    }
    
    getAllTests() {
        return this.tests;
    }
}

export function helperFunction() {
    // 헬퍼 함수
}

export const CONFIG = {
    maxTests: 10
};

import 할 때는 중괄호를 사용하고, 정확한 이름을 지정해야 합니다:

import { TestModel, helperFunction, CONFIG } from './models/TestModel.js';

Default Export

모듈에서 하나의 주요 값만 내보낼 때 사용합니다. 파일 하나당 하나의 default export만 가능합니다.

// utils/analytics.js
export default function trackEvent(eventName, data) {
    console.log('Event tracked:', eventName, data);
}

// 사용할 때
import trackEvent from './utils/analytics.js';
// 또는 원하는 이름으로 import 가능
import myTracker from './utils/analytics.js';

실전 적용: 프로젝트 구조

심리 테스트 프로젝트에서 저는 다음과 같이 모듈을 구성했습니다:

js/
├── app.js                    # 진입점
├── models/
│   └── TestModel.js         # 데이터 모델
├── views/
│   ├── CardView.js          # 카드 뷰
│   └── ModalView.js         # 모달 뷰
└── controllers/
    └── TestController.js    # 메인 컨트롤러

진입점 (app.js)

애플리케이션의 시작점입니다. Controller를 import하고 초기화합니다.

// js/app.js
import { TestController } from './controllers/TestController.js';

document.addEventListener('DOMContentLoaded', () => {
    const testController = new TestController();
    testController.init();
    
    // 디버깅을 위해 전역으로 등록
    window.testController = testController;
});

Controller에서 여러 모듈 import

Controller는 Model과 View 모두를 사용하므로 여러 모듈을 import합니다.

// js/controllers/TestController.js
import { TestModel } from '../models/TestModel.js';
import { CardView } from '../views/CardView.js';
import { ModalView } from '../views/ModalView.js';

export class TestController {
    constructor() {
        this.testModel = new TestModel();
        this.cardView = new CardView('test-cards-container');
        this.modalView = new ModalView('modal-container');
    }
    
    init() {
        const tests = this.testModel.getAllTests();
        this.cardView.render(tests, this.handleCardClick.bind(this));
    }
}

HTML에서 모듈 사용하기

ES6 모듈을 사용하려면 script 태그에 type="module" 속성을 추가해야 합니다.

<!-- index.html -->
<script type="module" src="js/app.js"></script>

중요: 모듈은 CORS 정책의 영향을 받으므로 로컬 파일 시스템에서는 작동하지 않습니다. 반드시 로컬 서버를 통해 실행해야 합니다.

# Python으로 로컬 서버 실행
python -m http.server 8000

# Node.js http-server 사용
npx http-server -p 8000

모듈 로딩 최적화

동적 import

모든 모듈을 처음부터 로드할 필요는 없습니다. 필요할 때만 로드하는 동적 import를 사용하면 초기 로딩 속도를 개선할 수 있습니다.

// 버튼 클릭 시 모듈 로드
button.addEventListener('click', async () => {
    const { TestResultView } = await import('./views/TestResultView.js');
    const resultView = new TestResultView();
    resultView.render(data);
});

트리 쉐이킹 (Tree Shaking)

모듈 시스템을 사용하면 번들러(Webpack, Rollup 등)가 사용하지 않는 코드를 자동으로 제거할 수 있습니다. 이를 통해 최종 번들 크기를 줄일 수 있습니다.

// utils.js
export function usedFunction() { }
export function unusedFunction() { } // 이 함수는 번들에서 제외됨

// main.js
import { usedFunction } from './utils.js';
usedFunction();

흔한 실수와 해결 방법

1. 파일 확장자 생략

Node.js와 달리 브라우저에서는 파일 확장자를 반드시 명시해야 합니다.

// ❌ 잘못된 예
import { TestModel } from './models/TestModel';

// ✅ 올바른 예
import { TestModel } from './models/TestModel.js';

2. 순환 의존성

A 모듈이 B를 import하고, B 모듈이 A를 import하는 순환 의존성은 피해야 합니다.

// ❌ 순환 의존성
// A.js
import { B } from './B.js';
export class A { }

// B.js
import { A } from './A.js';
export class B { }

// ✅ 해결: 공통 모듈 분리
// Common.js
export class Common { }

// A.js
import { Common } from './Common.js';
export class A extends Common { }

// B.js
import { Common } from './Common.js';
export class B extends Common { }

3. this 바인딩 문제

콜백 함수로 메서드를 전달할 때 this 바인딩에 주의해야 합니다.

// ❌ this가 undefined
this.cardView.render(tests, this.handleCardClick);

// ✅ bind 사용
this.cardView.render(tests, this.handleCardClick.bind(this));

// ✅ 화살표 함수 사용
this.cardView.render(tests, (test) => this.handleCardClick(test));

모듈 패턴의 실전 활용

프로젝트가 성장하면서 모듈 시스템의 진가를 실감했습니다. 새로운 View를 추가할 때 기존 코드를 전혀 수정하지 않고 새 파일만 만들면 됐습니다.

// 새로운 View 추가
// js/views/ChartView.js
export class ChartView {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
    }
    
    render(data) {
        // 차트 렌더링 로직
    }
}

// Controller에서 사용
import { ChartView } from '../views/ChartView.js';

this.chartView = new ChartView('chart-container');
this.chartView.render(chartData);
"모듈 시스템은 단순히 코드를 파일로 나누는 것이 아니라, 애플리케이션의 구조를 논리적으로 설계하는 도구입니다."

💡 핵심 요약

  • ES6 모듈은 export/import로 코드를 구조화합니다
  • type="module" 속성과 로컬 서버가 필요합니다
  • 파일 확장자(.js)를 반드시 명시해야 합니다
  • 동적 import로 성능을 최적화할 수 있습니다
  • 순환 의존성과 this 바인딩에 주의하세요