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) | |
|---|---|---|
| Syntax | require(), module.exports | import, export |
| Load time | require at runtime (path can vary) | Top-level import is static (great for tree shaking) |
| Typical use | Legacy Node and npm packages | Browser standard; Node with “type”: “module” |
Top-level this | module.exports | undefined (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 export | named export | |
|---|---|---|
| Count | One per file | Many |
| Import name | Can rename freely (import MyThing) | Names are fixed (use as for aliases) |
| Refactors / IDE | Easier to drift per file | Stable, easier to track |
| Good for | Single 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 Modules | CommonJS | |
|---|---|---|
| Syntax | import / export | require / module.exports |
| Runtimes | Browsers + Node | Node (legacy default) |
| Loading | Static (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;
importanalysis feels natural.
Quick chooser
| Situation | Pick |
|---|---|
| New SPA, fast prototype | Start with Vite |
| Deep investment in Webpack plugins | Keep Webpack or migrate gradually |
| Publishing a library | Rollup / 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
- ES Modules:
export/import, named vs default - CommonJS:
module.exports/require()(Node legacy default) - Browser:
<script type="module">,await import()for lazy chunks - Patterns: barrel
index.js, layered API clients, shared utils
Next steps
Related posts
- Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
- C++20 Modules 완벽 가이드 | 헤더 파일을 넘어서
- C++20 Modules
- C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
- JavaScript 클래스 | ES6 Class 문법 완벽 정리
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.
Related Articles (Internal Links)
Other articles related to this topic.
- JavaScript Classes | ES6 Class Syntax Explained
- JavaScript Error Handling | try/catch· the Error Object
- JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리
Keywords Covered in This Article (Related Search Terms)
This article covers JavaScript, Modules, ES6, import, export, CommonJS, Bundler.