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

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

이 글의 핵심

JavaScript 모듈: ES6 Modules, CommonJS CommonJS와 ES Modules 한눈에부터 핵심 개념·패턴·실무 함정을 코드 예제로 풉니다.

들어가며

모듈이란?

모듈(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: 내보내기

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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: 가져오기

다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 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

다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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}!`);
}

아래 코드는 javascript를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// main.js
// Default import (이름 자유롭게)
import User from './user.js';
const user = new User("홍길동", "hong@test.com");
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

다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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”

다음은 html를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

<!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

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 조건부 로딩
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: 유틸리티 모듈

다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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, ');
}

다음은 javascript를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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;
}

아래 코드는 javascript를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// utils/index.js (배럴 파일)
export * from './string.js';
export * from './array.js';
// 또는 선택적으로
export { capitalize, truncate } from './string.js';
export { chunk, unique } from './array.js';

아래 코드는 javascript를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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 클라이언트

다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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;

다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// api/users.js
// 필요한 모듈 import
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}`);
    }
}

다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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: "hong@test.com"
        });
        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 설정

아래 코드는 json를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

또는 파일 확장자를 .mjs로 사용: 아래 코드는 javascript를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

실수 1: 순환 참조

다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 순환 참조
// 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: 잘못된 경로

아래 코드는 javascript를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 확장자 누락 (브라우저)
import { add } from './math';  // 에러!
// ✅ 확장자 포함
import { add } from './math.js';
// Node.js는 확장자 생략 가능 (CommonJS)
const math = require('./math');  // OK

실수 3: export default 여러 개

아래 코드는 javascript를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 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 클라이언트
    • 유틸리티 모듈

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3