JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리

JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리

이 글의 핵심

JavaScript 모듈에 대한 실전 가이드입니다. ES6 Modules, CommonJS 완벽 정리 등을 예제와 함께 상세히 설명합니다.

들어가며

모듈이란?

모듈(Module)은 파일(또는 단위) 단위로 책임을 나누어 다시 가져다 쓰기 쉽게 만든 구조입니다.

모듈을 쓰면 얻는 점:

  • 코드 재사용: 한 번 작성, 여러 곳에서 사용
  • 네임스페이스: 변수명 충돌 방지
  • 유지보수: 관련 코드를 함께 관리
  • 의존성 관리: 필요한 모듈만 로드

CommonJS와 ES Modules 한눈에

항목CommonJS (CJS)ES Modules (ESM)
문법require(), module.exportsimport, export
로딩 시점런타임에 경로를 바꿔 require 가능정적 import파일 상단에서 분석(트리 쉐이킹에 유리)
대표 환경Node.js 레거시·npm 패키지브라우저 표준, Node ("type": "module")
this (최상위)module.exportsundefined(엄격 모듈)
  • 새 프로젝트: 가능하면 ESM을 기본으로 하고, 구형 패키지는 import x from 'pkg'가 안 될 때만 **동적 import()**나 interop을 씁니다.
  • Node에서 혼용: 한 파일이 CJS냐 ESM이냐는 package.json"type"과 확장자(.cjs / .mjs)로 갈립니다.

1. ES6 Modules (ES Modules)

export: 내보내기

// math.js

// Named export (여러 개 가능)
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export const PI = 3.14159;

// 한 번에 export
function multiply(a, b) {
    return a * b;
}

function divide(a, b) {
    return a / b;
}

export { multiply, divide };

// 이름 변경하여 export
function power(a, b) {
    return a ** b;
}

export { power as pow };

import: 가져오기

// main.js

// Named import
import { add, subtract, PI } from './math.js';

console.log(add(10, 20));      // 30
console.log(subtract(20, 10)); // 10
console.log(PI);               // 3.14159

// 이름 변경하여 import
import { pow as power } from './math.js';
console.log(power(2, 3));  // 8

// 모두 import
import * as math from './math.js';
console.log(math.add(5, 3));  // 8
console.log(math.PI);         // 3.14159

// 부작용만 실행 (내보내는 값 없이 초기화 코드만)
import './polyfills.js';

// 다른 모듈에서 다시 내보내기(re-export)
// export { add } from './math.js';
// export { default as User } from './user.js';

default export

// user.js

// Default export (모듈당 1개만)
export default class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
    
    greet() {
        console.log(`Hello, ${this.name}!`);
    }
}

// 또는
class User {
    // ...
}

export default User;

// 함수도 가능
export default function greet(name) {
    console.log(`Hello, ${name}!`);
}
// main.js

// Default import (이름 자유롭게)
import User from './user.js';

const user = new User("홍길동", "[email protected]");
user.greet();  // Hello, 홍길동!

// Named + Default 함께
import User, { formatDate, validateEmail } from './user.js';

default export vs named export 선택 가이드

default exportnamed export
개수파일당 하나여러 개
가져올 때 이름import MyThing처럼 자유롭게 바꿔 쓰기 쉬움import { a, b }처럼 이름이 고정(별칭 as 가능)
자동완성·리팩터이름이 파일마다 달라질 수 있어 추적이 약함IDE가 정확히 따라감
적합한 경우React 단일 컴포넌트, “이 파일의 대표 하나”유틸 함수 모음, 여러 상수·타입

실무 팁: 라이브러리 API는 named가 리팩터링에 유리하고, 앱 코드에서 페이지 단위 컴포넌트 하나만 내보낼 때 default를 쓰는 패턴도 흔합니다. 한 파일에서 default와 named를 섞는 것도 가능합니다.


2. CommonJS (Node.js)

module.exports

// math.js

function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

const PI = 3.14159;

// 방법 1: 객체로 내보내기
module.exports = {
    add,
    subtract,
    PI
};

// 방법 2: 개별 할당
module.exports.add = add;
module.exports.subtract = subtract;
module.exports.PI = PI;

// 방법 3: exports 축약
exports.add = add;
exports.subtract = subtract;

require

// main.js

// 전체 가져오기
const math = require('./math.js');
console.log(math.add(10, 20));  // 30
console.log(math.PI);           // 3.14159

// 구조 분해
const { add, subtract } = require('./math.js');
console.log(add(10, 20));  // 30

// 내장 모듈
const fs = require('fs');
const path = require('path');
const http = require('http');

ES Modules vs CommonJS

특징ES ModulesCommonJS
문법import/exportrequire/module.exports
환경브라우저 + Node.jsNode.js
로딩정적 (컴파일 타임)동적 (런타임)
비동기
Tree Shaking
파일 확장자.mjs 또는 package.json.js

Interop 요약: ESM에서 CJS 모듈을 가져올 때는 환경에 따라 default로 한 번 감싸진 값이 올 수 있습니다(import pkg from 'cjs-pkg'). 반대로 CJS에서 ESM만 제공하는 패키지는 **import()로 비동기 로드하거나, Node 문서의 createRequire**를 참고합니다.


3. 브라우저에서 모듈 사용

type=“module”

<!DOCTYPE html>
<html>
<head>
    <title>ES Modules</title>
</head>
<body>
    <h1>모듈 테스트</h1>
    
    <script type="module">
        // math.js에서 import
        import { add, subtract } from './math.js';
        
        console.log(add(10, 20));  // 30
        
        // 동적 import
        const button = document.querySelector('#loadBtn');
        button.addEventListener('click', async () => {
            const module = await import('./heavy-module.js');
            module.doSomething();
        });
    </script>
</body>
</html>

동적 import

// 조건부 로딩
async function loadModule(moduleName) {
    if (moduleName === 'math') {
        const math = await import('./math.js');
        return math;
    }
}

// 사용
const math = await loadModule('math');
console.log(math.add(5, 3));

// 코드 스플리팅
button.addEventListener('click', async () => {
    const { default: Chart } = await import('./chart.js');
    new Chart('#myChart');
});

4. 실전 예제

예제 1: 유틸리티 모듈

// utils/string.js
export function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

export function truncate(str, maxLength) {
    if (str.length <= maxLength) return str;
    return str.slice(0, maxLength) + '...';
}

export function slugify(str) {
    return str
        .toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[^\w-]/g, '');
}
// utils/array.js
export function chunk(arr, size) {
    const result = [];
    for (let i = 0; i < arr.length; i += size) {
        result.push(arr.slice(i, i + size));
    }
    return result;
}

export function unique(arr) {
    return [...new Set(arr)];
}

export function shuffle(arr) {
    const copy = [...arr];
    for (let i = copy.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [copy[i], copy[j]] = [copy[j], copy[i]];
    }
    return copy;
}
// utils/index.js (배럴 파일)
export * from './string.js';
export * from './array.js';

// 또는 선택적으로
export { capitalize, truncate } from './string.js';
export { chunk, unique } from './array.js';
// main.js
import { capitalize, chunk } from './utils/index.js';

console.log(capitalize("hello"));  // Hello
console.log(chunk([1, 2, 3, 4, 5], 2));  // [[1, 2], [3, 4], [5]]

예제 2: API 클라이언트

// api/client.js
const BASE_URL = 'https://api.example.com';

export class APIClient {
    constructor(apiKey) {
        this.apiKey = apiKey;
    }
    
    async request(endpoint, options = {}) {
        const url = `${BASE_URL}${endpoint}`;
        const headers = {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.apiKey}`,
            ...options.headers
        };
        
        const response = await fetch(url, { ...options, headers });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return await response.json();
    }
    
    get(endpoint) {
        return this.request(endpoint, { method: 'GET' });
    }
    
    post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }
    
    put(endpoint, data) {
        return this.request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }
    
    delete(endpoint) {
        return this.request(endpoint, { method: 'DELETE' });
    }
}

export default APIClient;
// api/users.js
import APIClient from './client.js';

export class UserAPI {
    constructor(apiKey) {
        this.client = new APIClient(apiKey);
    }
    
    async getUsers() {
        return await this.client.get('/users');
    }
    
    async getUser(id) {
        return await this.client.get(`/users/${id}`);
    }
    
    async createUser(userData) {
        return await this.client.post('/users', userData);
    }
    
    async updateUser(id, userData) {
        return await this.client.put(`/users/${id}`, userData);
    }
    
    async deleteUser(id) {
        return await this.client.delete(`/users/${id}`);
    }
}
// main.js
import { UserAPI } from './api/users.js';

const api = new UserAPI('your-api-key');

async function main() {
    try {
        const users = await api.getUsers();
        console.log(users);
        
        const newUser = await api.createUser({
            name: "홍길동",
            email: "[email protected]"
        });
        console.log("생성됨:", newUser);
    } catch (error) {
        console.error("에러:", error);
    }
}

main();

번들러와 빌드: Webpack, Vite

브라우저는 ESM을 네이티브로 로드할 수 있지만, 실제 프로덕션에서는 다음 이유로 번들러를 씁니다.

  • 여러 파일·npm 패키지한(또는 몇) 개의 JS 파일으로 묶기
  • 난독화·압축, 코드 스플리팅(동적 import 단위로 청크 분리)
  • TypeScript·JSX, CSS import 등 전처리

Webpack

  • 역할: 진입점(entry)부터 의존성 그래프를 따라 모듈을 묶는 도구입니다. 로더(CSS, TS 등)와 플러그인으로 확장합니다.
  • 특징: 설정이 세밀하게 가능하고, 대규모 레거시 프로젝트에서 여전히 많이 씁니다.
  • 개발 서버: webpack-dev-server로 HMR(핫 리로드) 등을 구성합니다.

Vite

  • 역할: 개발 시 네이티브 ESM으로 빠르게 서빙하고, 프로덕션 빌드는 Rollup 기반으로 묶는 방식이 일반적입니다.
  • 특징: 초기 설정이 단순하고, Vue/React 템플릿과 궁합이 좋습니다. import 분석이 자연스러워 DX(개발 경험)가 가볍습니다.

선택 가이드 (요약)

상황추천
새 SPA, 빠른 프로토타입Vite부터 검토
기존 Webpack 설정·플러그인에 깊이 의존Webpack 유지 또는 점진적 마이그레이션
라이브러리만 배포Rollup/tsup 등도 선택지

동적 import()는 Webpack/Vite 모두에서 별도 청크로 떼어 내는 데 자주 쓰입니다.


5. Node.js에서 ES Modules 사용

package.json 설정

{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module"
}

또는 파일 확장자를 .mjs로 사용:

// math.mjs
export function add(a, b) {
    return a + b;
}
// main.mjs
import { add } from './math.mjs';
console.log(add(10, 20));

6. 자주 하는 실수와 해결법

실수 1: 순환 참조

// ❌ 순환 참조
// a.js
import { b } from './b.js';
export const a = 'A';

// b.js
import { a } from './a.js';
export const b = 'B';

// ✅ 해결: 구조 개선
// common.js
export const a = 'A';
export const b = 'B';

// a.js
import { b } from './common.js';

// b.js
import { a } from './common.js';

실수 2: 잘못된 경로

// ❌ 확장자 누락 (브라우저)
import { add } from './math';  // 에러!

// ✅ 확장자 포함
import { add } from './math.js';

// Node.js는 확장자 생략 가능 (CommonJS)
const math = require('./math');  // OK

실수 3: export default 여러 개

// ❌ default는 1개만
export default function add() {}
export default function subtract() {}  // SyntaxError

// ✅ named export 사용
export function add() {}
export function subtract() {}

정리

핵심 요약

  1. ES Modules:

    • export: 내보내기
    • import: 가져오기
    • Named export: 여러 개
    • Default export: 1개
  2. CommonJS:

    • module.exports: 내보내기
    • require(): 가져오기
    • Node.js 기본
  3. 브라우저:

    • <script type="module">
    • 동적 import: await import()
  4. 패턴:

    • 배럴 파일: index.js
    • API 클라이언트
    • 유틸리티 모듈

다음 단계

  • JavaScript 에러 처리
  • JavaScript 디자인 패턴
  • TypeScript 시작하기

관련 글

  • Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
  • C++20 Modules 완벽 가이드 | 헤더 파일을 넘어서
  • C++20 Modules |
  • C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
  • JavaScript 클래스 | ES6 Class 문법 완벽 정리