[2026] JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리
이 글의 핵심
JavaScript 모듈: ES6 Modules, CommonJS CommonJS와 ES Modules 한눈에부터 핵심 개념·패턴·실무 함정을 코드 예제로 풉니다.
들어가며
모듈이란?
모듈(Module)은 파일(또는 단위) 단위로 책임을 나누어 다시 가져다 쓰기 쉽게 만든 구조입니다. 모듈을 쓰면 얻는 점:
- ✅ 코드 재사용: 한 번 작성, 여러 곳에서 사용
- ✅ 네임스페이스: 변수명 충돌 방지
- ✅ 유지보수: 관련 코드를 함께 관리
- ✅ 의존성 관리: 필요한 모듈만 로드
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
CommonJS와 ES Modules 한눈에
| 항목 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 문법 | require(), module.exports | import, export |
| 로딩 시점 | 런타임에 경로를 바꿔 require 가능 | 정적 import는 파일 상단에서 분석(트리 쉐이킹에 유리) |
| 대표 환경 | Node.js 레거시·npm 패키지 | 브라우저 표준, Node ("type": "module") |
this (최상위) | module.exports | undefined(엄격 모듈) |
- 새 프로젝트: 가능하면 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 export | named 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 Modules | CommonJS |
|---|---|---|
| 문법 | import/export | require/module.exports |
| 환경 | 브라우저 + Node.js | Node.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() {}
정리
핵심 요약
- ES Modules:
export: 내보내기import: 가져오기- Named export: 여러 개
- Default export: 1개
- CommonJS:
module.exports: 내보내기require(): 가져오기- Node.js 기본
- 브라우저:
<script type="module">- 동적 import:
await import()
- 패턴:
- 배럴 파일:
index.js - API 클라이언트
- 유틸리티 모듈
- 배럴 파일: