본문으로 건너뛰기 JavaScript Modules | ES Modules and CommonJS Explained

JavaScript Modules | ES Modules and CommonJS Explained

JavaScript Modules | ES Modules and CommonJS Explained

이 글의 핵심

JavaScript modules: ES import/export vs CommonJS require, browser type=module, dynamic import(), bundlers (Webpack vs Vite), barrels, and Node type: module setup.

Introduction

A module splits code into reusable units with explicit boundaries. Why modules:

  • Reuse: write once, import many times
  • Namespaces: fewer global name collisions
  • Maintainability: colocate related code
  • Dependencies: load only what you need

CommonJS vs ES Modules at a glance

CommonJS (CJS)ES Modules (ESM)
Syntaxrequire(), module.exportsimport, export
Load timerequire at runtime (path can vary)Top-level import is static (great for tree shaking)
Typical useLegacy Node and npm packagesBrowser standard; Node with “type”: “module”
Top-level thismodule.exportsundefined (module scope is strict)
  • New projects: prefer ESM; fall back to dynamic import() or interop for older packages.
  • Node: whether a file is CJS or ESM is determined by package.json "type" and extensions (.cjs / .mjs).

1. ES Modules

export

// math.js
// Named exports (many allowed)
export function add(a, b) {
    return a + b;
}
export function subtract(a, b) {
    return a - b;
}
export const PI = 3.14159;
// Export list
function multiply(a, b) {
    return a * b;
}
function divide(a, b) {
    return a / b;
}
export { multiply, divide };
// Rename on export
function power(a, b) {
    return a ** b;
}
export { power as pow };

import

// main.js
// Named imports
import { add, subtract, PI } from './math.js';
console.log(add(10, 20));      // 30
console.log(subtract(20, 10)); // 10
console.log(PI);               // 3.14159
// Rename on import
import { pow as power } from './math.js';
console.log(power(2, 3));  // 8
// Namespace import
import * as math from './math.js';
console.log(math.add(5, 3));  // 8
console.log(math.PI);         // 3.14159
// Side-effect import (initialization only)
import './polyfills.js';
// Re-export from another module
// export { add } from './math.js';
// export { default as User } from './user.js';

Default export

// user.js
// One default export per module
export default class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
    
    greet() {
        console.log(`Hello, ${this.name}!`);
    }
}
// Or
class User {
    // ...
}
export default User;
// Functions work too
export default function greet(name) {
    console.log(`Hello, ${name}!`);
}
// main.js
// Default import (local name is yours)
import User from './user.js';
const user = new User("홍길동", "hong@test.com");
user.greet();  // Hello, 홍길동!
// Default + named together
import User, { formatDate, validateEmail } from './user.js';

Choosing default vs named

default exportnamed export
CountOne per fileMany
Import nameCan rename freely (import MyThing)Names are fixed (use as for aliases)
Refactors / IDEEasier to drift per fileStable, easier to track
Good forSingle React component, “one main thing”Utility collections, constants, types
Tip: libraries often favor named exports; app code sometimes uses default for a page component. Mixing both in one file is valid.

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;
// Option 1: export an object
module.exports = {
    add,
    subtract,
    PI
};
// Option 2: attach properties
module.exports.add = add;
module.exports.subtract = subtract;
module.exports.PI = PI;
// Option 3: exports shorthand
exports.add = add;
exports.subtract = subtract;

require

// main.js
// Whole module
const math = require('./math.js');
console.log(math.add(10, 20));  // 30
console.log(math.PI);           // 3.14159
// Destructuring
const { add, subtract } = require('./math.js');
console.log(add(10, 20));  // 30
// Built-ins
const fs = require('fs');
const path = require('path');
const http = require('http');

ESM vs CJS

ES ModulesCommonJS
Syntaximport / exportrequire / module.exports
RuntimesBrowsers + NodeNode (legacy default)
LoadingStatic (parse time)Dynamic (runtime)
Async loading✅ (import())
Tree shaking
Extensions.mjs or "type" in package.json.js (CJS default)
Interop: importing CJS from ESM may wrap the namespace under default depending on tooling. Pure-ESM packages consumed from CJS may need import() or Node’s createRequire.

3. Modules in the browser

type="module"

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

Dynamic import()

// Conditional loading
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));
// Code splitting
button.addEventListener('click', async () => {
    const { default: Chart } = await import('./chart.js');
    new Chart('#myChart');
});

4. Practical examples

Example 1: Utility modules

// 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 (barrel)
export * from './string.js';
export * from './array.js';
// Or selective re-exports
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]]

Example 2: API client

// 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
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: "hong@test.com"
        });
        console.log("생성됨:", newUser);
    } catch (error) {
        console.error("에러:", error);
    }
}
main();

Bundlers and builds: Webpack, Vite

Browsers can load ESM natively, but production apps usually use a bundler because:

  • Bundle many files and npm packages into one (or few) assets
  • Minify, split chunks (often aligned with dynamic import())
  • Transpile TypeScript/JSX, process CSS imports, etc.

Webpack

  • Role: walk the dependency graph from entry and emit bundles; extend with loaders and plugins.
  • Fit: very configurable; still common for large legacy codebases.
  • Dev: webpack-dev-server, HMR, etc.

Vite

  • Role: dev server uses native ESM for speed; production builds often use Rollup under the hood.
  • Fit: minimal config, great DX with Vue/React templates; import analysis feels natural.

Quick chooser

SituationPick
New SPA, fast prototypeStart with Vite
Deep investment in Webpack pluginsKeep Webpack or migrate gradually
Publishing a libraryRollup / tsup, etc.
Dynamic import() typically becomes a separate chunk in both Webpack and Vite.

5. ES modules in Node.js

package.json

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

Or use the .mjs extension:

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

6. Common pitfalls

Pitfall 1: circular dependencies

// ❌ Circular
// a.js
import { b } from './b.js';
export const a = 'A';
// b.js
import { a } from './a.js';
export const b = 'B';
// ✅ Fix structure
// common.js
export const a = 'A';
export const b = 'B';
// a.js
import { b } from './common.js';
// b.js
import { a } from './common.js';

Pitfall 2: wrong paths

// ❌ Missing extension (browser)
import { add } from './math';  // Error!
// ✅ Include extension
import { add } from './math.js';
// Node may resolve extensionless paths (CommonJS)
const math = require('./math');  // OK

Pitfall 3: multiple defaults

// ❌ Only one default
export default function add() {}
export default function subtract() {}  // SyntaxError
// ✅ Use named exports
export function add() {}
export function subtract() {}

Summary

Key takeaways

  1. ES Modules: export / import, named vs default
  2. CommonJS: module.exports / require() (Node legacy default)
  3. Browser: <script type="module">, await import() for lazy chunks
  4. Patterns: barrel index.js, layered API clients, shared utils

Next steps



Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. JavaScript modules: ES import/export vs CommonJS require, browser type=module, dynamic import(), bundlers (Webpack vs.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the JavaScript series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers JavaScript, Modules, ES6, import, export, CommonJS, Bundler.