Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
이 글의 핵심
Node.js 모듈 시스템: CommonJS와 ES Modules CommonJS 모듈·ES Modules.
들어가며
모듈이란?
모듈은 재사용 가능한 코드 조각을 파일로 분리한 것입니다. Node.js는 두 가지 모듈 시스템을 지원합니다:
- CommonJS: Node.js 기본 방식 (
require,module.exports) - ES Modules: JavaScript 표준 방식 (
import,export) 한 프로젝트 안에서 도구를 주머니마다 나눠 담듯 파일을 나누면, 이름이 겹치지 않고(네임스페이스), 테스트와 교체가 쉬워집니다.require('express')처럼 이름만 말하면node_modules에서 꺼내 오는 것은, 공용 서랍에서 표준 부품을 가져오는 것과 비슷합니다. npm·package-lock.json은 의존성 해석·재현 설치의 중심입니다. 같은 축에서 보면 Python pip·uv·Poetry·Go 모듈·go.sum·Rust Cargo가 있으며, C++는 CMake와 Conan·vcpkg 조합이 자주 사용됩니다. 빌드 철학 비교는 C++ 빌드 시스템 완전 비교를 참고하세요. 모듈의 장점
- ✅ 코드 재사용: 한 번 작성, 여러 곳에서 사용
- ✅ 네임스페이스: 전역 스코프 오염 방지
- ✅ 유지보수: 기능별로 파일 분리
- ✅ 의존성 관리: 명확한 의존 관계
- ✅ 테스트: 독립적인 단위 테스트 가능
1. CommonJS 모듈
require()의 내부 동작 메커니즘
Node.js 모듈 로딩 과정:
require('./math') 실행 시 내부 동작:
1. 모듈 경로 해석 (Module Resolution):
require('./math') →
a. 확장자 추가 시도:
- ./math → 실패
- ./math.js → 성공! (파일 존재)
- ./math.json (시도 안 함, .js 먼저 찾음)
- ./math.node (네이티브 모듈, 마지막 시도)
b. 디렉토리인 경우:
- ./math/package.json → "main" 필드 확인
- ./math/index.js → 기본값
require('express') (패키지명) →
현재 디렉토리부터 상위로 node_modules 탐색:
/home/user/project/app.js에서 require('express'):
1. /home/user/project/node_modules/express
2. /home/user/node_modules/express
3. /home/node_modules/express
4. /node_modules/express
찾으면 중단, 없으면 "Cannot find module 'express'" 에러
2. 캐시 확인 (Module Cache):
require.cache 객체에 이미 로드된 모듈 확인:
require.cache['/home/user/project/math.js'] → 있음?
→ 캐시된 module.exports 즉시 반환 (재실행 안 함)
없음?
→ 3단계로 진행
3. 파일 읽기 및 래핑:
const content = fs.readFileSync('/home/user/project/math.js', 'utf8');
Node.js가 모듈 코드를 함수로 감쌈:
(function(exports, require, module, __filename, __dirname) {
// 여기에 math.js 코드 삽입
function add(a, b) {
return a + b;
}
module.exports = { add };
});
5개 파라미터 자동 제공:
- exports: module.exports 참조
- require: 다른 모듈 로드 함수
- module: 현재 모듈 객체
- __filename: 현재 파일 절대 경로
- __dirname: 현재 디렉토리 절대 경로
4. 코드 실행:
const module = { exports: {} };
const exports = module.exports;
wrapper(exports, require, module, __filename, __dirname);
→ math.js 코드 실행
→ module.exports에 함수/변수 할당
5. 캐시 저장:
require.cache['/home/user/project/math.js'] = module;
다음 require('./math') 호출 시 즉시 반환
6. module.exports 반환:
return module.exports;
require.cache의 역할:
// 캐시 확인
console.log(require.cache);
// {
// '/home/user/project/math.js': Module {
// id: '/home/user/project/math.js',
// exports: { add: [Function], subtract: [Function] },
// loaded: true,
// children: [],
// paths: [...]
// }
// }
// 캐시 삭제 (재로드하고 싶을 때)
delete require.cache[require.resolve('./math')];
// 다음 require('./math') 호출 시 재실행됨
순환 참조 시 동작:
// a.js
console.log('a.js 시작');
exports.done = false;
const b = require('./b');
console.log('b.done:', b.done);
exports.done = true;
console.log('a.js 종료');
// b.js
console.log('b.js 시작');
exports.done = false;
const a = require('./a'); // 순환!
console.log('a.done:', a.done);
exports.done = true;
console.log('b.js 종료');
// main.js
const a = require('./a');
// 출력:
// a.js 시작
// b.js 시작
// a.done: false ← a.js가 아직 완료 안 됨!
// b.js 종료
// b.done: true
// a.js 종료
// Node.js는 순환 참조 시:
// - 캐시에 미완성 module.exports 저장
// - 재귀 방지 (무한 루프 회피)
// - 부분 완성된 객체 반환 (done=false)
기본 사용법
내보내기 (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
};
가져오기 (require):
// app.js
const math = require('./math');
// 내부 동작:
// 1. './math' 경로 해석 → /absolute/path/to/math.js
// 2. require.cache 확인 → 없음
// 3. fs.readFileSync로 파일 읽기
// 4. 함수로 래핑
// 5. 코드 실행 → module.exports 설정
// 6. 캐시 저장
// 7. module.exports 반환
console.log(math.add(10, 5)); // 15
console.log(math.subtract(10, 5)); // 5
console.log(math.PI); // 3.14159
exports vs module.exports
exports와 module.exports의 차이를 정확히 이해하는 것이 중요합니다:
// ✅ exports 사용 (속성 추가)
// exports는 module.exports를 가리키는 참조 변수
// 속성을 추가하는 방식으로만 사용 가능
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
// 결과: { add: [Function], subtract: [Function] }
// ✅ module.exports 사용 (전체 교체)
// module.exports는 실제로 반환되는 객체
// 완전히 새로운 객체로 교체 가능
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
// 결과: { add: [Function], subtract: [Function] }
// ❌ 잘못된 사용
exports = {
add: (a, b) => a + b // 작동 안 함!
};
// exports 변수에 새 객체를 할당하면
// module.exports와의 참조가 끊어짐
// require()는 module.exports를 반환하므로 이 변경사항은 무시됨
올바른 이해: Node.js가 모듈을 로드할 때 내부적으로 어떻게 동작하는지 이해하면 혼란을 피할 수 있습니다:
// Node.js 내부 동작 (개념적 설명)
function require(modulePath) {
// 1. 빈 module 객체 생성
const module = { exports: {} };
// 2. exports는 module.exports를 참조
// 이것이 핵심! exports는 단순히 참조 변수
const exports = module.exports;
// 3. 모듈 코드를 함수로 감싸서 실행
(function(module, exports) {
// 여기서 실제 모듈 코드가 실행됨
exports.add = ...; // ✅ OK - module.exports에 속성 추가
module.exports = ...; // ✅ OK - module.exports 자체를 교체
exports = ...; // ❌ 안 됨 - exports 참조만 끊김, module.exports는 그대로
})(module, exports);
// 4. 최종적으로 module.exports를 반환
// exports가 아닌 module.exports를 반환!
return module.exports;
}
핵심 규칙
exports.xxx = ...→ 속성 추가 (OK)module.exports = ...→ 전체 교체 (OK)exports = ...→ 참조만 끊김 (작동 안 함)
다양한 내보내기 패턴
패턴 1: 여러 함수 내보내기
// utils.js
exports.formatDate = (date) => {
return date.toISOString().split('T')[0];
};
exports.capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
exports.randomInt = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
패턴 2: 클래스 내보내기
// user.js
// 타입 정의
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `안녕하세요, ${this.name}님!`;
}
}
module.exports = User;
// app.js
const User = require('./user');
const user = new User('홍길동', 'hong@example.com');
console.log(user.greet());
패턴 3: 싱글톤 패턴
// database.js
class Database {
constructor() {
this.connection = null;
}
connect() {
if (!this.connection) {
this.connection = { connected: true };
console.log('데이터베이스 연결됨');
}
return this.connection;
}
}
// 싱글톤 인스턴스 내보내기
module.exports = new Database();
// app.js
const db = require('./database');
db.connect(); // 첫 연결
// 다른 파일에서도 같은 인스턴스
const db2 = require('./database');
db2.connect(); // 이미 연결됨 (같은 인스턴스)
패턴 4: 팩토리 함수
// logger.js
// 함수 정의 및 구현
function createLogger(prefix) {
return {
log: (message) => {
console.log(`[${prefix}] ${message}`);
},
error: (message) => {
console.error(`[${prefix}] ERROR: ${message}`);
}
};
}
module.exports = createLogger;
// app.js
const createLogger = require('./logger');
const appLogger = createLogger('APP');
const dbLogger = createLogger('DB');
appLogger.log('서버 시작');
dbLogger.log('데이터베이스 연결');
2. ES Modules
ES Modules 로딩 메커니즘
import의 내부 동작 (CommonJS와 다른 점):
import { add } from './math.mjs' 실행 시:
1. 정적 분석 단계 (Parse Time):
코드 실행 전에 import/export 문만 스캔:
import { add } from './math.mjs' ← 최상위에만 가능
export function subtract() {} ← 최상위에만 가능
→ 의존성 그래프 구축
→ 순환 참조 감지
→ 트리 쉐이킹 준비 (사용 안 하는 export 제거)
CommonJS는 런타임에 동적 로드:
if (condition) {
const math = require('./math'); // ✅ 가능
}
ES Modules는 정적 (컴파일 타임):
if (condition) {
import { add } from './math.mjs'; // ❌ 에러!
}
동적 로드는 import() 함수 사용:
if (condition) {
const { add } = await import('./math.mjs'); // ✅ 가능
}
2. 모듈 그래프 구축:
app.mjs:
import './math.mjs'
import './utils.mjs'
math.mjs:
import './constants.mjs'
utils.mjs:
import './helpers.mjs'
의존성 그래프:
app.mjs
├─ math.mjs
│ └─ constants.mjs
└─ utils.mjs
└─ helpers.mjs
→ 깊이 우선 탐색 (DFS)로 로드 순서 결정
→ constants.mjs → math.mjs → helpers.mjs → utils.mjs → app.mjs
3. 모듈 인스턴스화 (Instantiation):
각 모듈의 export를 메모리에 생성:
math.mjs:
export function add() {}
export const PI = 3.14
→ Module Record 생성:
{
exports: {
add: [uninitialized], ← 아직 실행 안 됨
PI: [uninitialized]
}
}
Live Binding (핵심 특징!):
- export된 값은 "참조"로 연결
- 원본 변경 시 import 측에서도 즉시 반영
4. 모듈 실행 (Evaluation):
의존성 순서대로 코드 실행:
constants.mjs 실행 → export 값 초기화
math.mjs 실행 → export 값 초기화
helpers.mjs 실행 → export 값 초기화
utils.mjs 실행 → export 값 초기화
app.mjs 실행
각 모듈은 딱 한 번만 실행 (캐시)
5. import 해석:
import { add } from './math.mjs'
→ add는 math.mjs의 add를 참조
→ Live Binding이므로 math.mjs에서 add 변경 시 즉시 반영
CommonJS vs ES Modules 핵심 차이:
로딩 시점:
CommonJS (런타임):
const math = require('./math');
console.log(math.add(1, 2));
→ require가 실행될 때 파일 읽기
→ 조건문 안에서 동적 로드 가능
→ 느린 시작 시간
ES Modules (파싱 타임):
import { add } from './math.mjs';
console.log(add(1, 2));
→ 코드 실행 전에 모든 import 해석
→ 정적 분석 가능 (번들러, 트리 쉐이킹)
→ 빠른 최적화
바인딩 방식:
CommonJS (값 복사):
// math.js
let counter = 0;
exports.increment = () => ++counter;
exports.getCounter = () => counter;
// app.js
const math = require('./math');
console.log(math.getCounter()); // 0
math.increment();
console.log(math.getCounter()); // 1
ES Modules (Live Binding):
// math.mjs
export let counter = 0;
export function increment() { ++counter; }
// app.mjs
import { counter, increment } from './math.mjs';
console.log(counter); // 0
increment();
console.log(counter); // 1 ← 자동 업데이트!
모듈 캐싱:
CommonJS:
- require.cache 객체에 저장
- 수동 삭제 가능 (재로드)
- 순환 참조 시 미완성 exports 반환
ES Modules:
- 내부 Module Map에 저장
- 캐시 삭제 불가 (안정성)
- 순환 참조 시 Live Binding으로 해결
기본 사용법
내보내기 (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
};
가져오기 (require):
// app.js
const math = require('./math');
// 내부 동작:
// 1. './math' 경로 해석 → /absolute/path/to/math.js
// 2. require.cache 확인 → 없음
// 3. fs.readFileSync로 파일 읽기
// 4. 함수로 래핑:
// (function(exports, require, module, __filename, __dirname) {
// // math.js 코드
// })
// 5. 코드 실행 → module.exports 설정
// 6. require.cache에 저장
// 7. module.exports 반환 (값 복사)
console.log(math.add(10, 5)); // 15
console.log(math.subtract(10, 5)); // 5
console.log(math.PI); // 3.14159
Named Export
// math.mjs
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// 또는 한 번에
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
return a / b;
}
export { multiply, divide };
가져오기:
// app.mjs
import { add, subtract, PI } from './math.mjs';
console.log(add(10, 5)); // 15
// 이름 변경
import { add as plus } from './math.mjs';
console.log(plus(10, 5)); // 15
// 모두 가져오기
import * as math from './math.mjs';
console.log(math.add(10, 5));
Default Export
// calculator.mjs
export default class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
// Named + Default
export const VERSION = '1.0.0';
가져오기:
// app.mjs
import Calculator, { VERSION } from './calculator.mjs';
const calc = new Calculator();
console.log(calc.add(10, 5)); // 15
console.log(VERSION); // 1.0.0
동적 import
// 조건부 로딩
async function loadModule() {
if (condition) {
const module = await import('./heavy-module.mjs');
module.doSomething();
}
}
// 지연 로딩
button.addEventListener('click', async () => {
const { processData } = await import('./data-processor.mjs');
processData();
});
3. CommonJS vs ES Modules
비교표
| 특징 | CommonJS | ES Modules |
|---|---|---|
| 문법 | require, module.exports | import, export |
| 로딩 | 동기 (런타임) | 비동기 (정적 분석) |
| 파일 확장자 | .js | .mjs 또는 .js (with "type": "module") |
| 기본 내보내기 | module.exports = ... | export default ... |
| Named 내보내기 | exports.name = ... | export const name = ... |
| 동적 로딩 | ✅ (기본) | ✅ (import()) |
| 트리 쉐이킹 | ❌ | ✅ |
| 브라우저 지원 | ❌ | ✅ |
| Node.js 지원 | ✅ (기본) | ✅ (v12+) |
언제 무엇을 사용할까?
CommonJS 사용:
- ✅ 기존 Node.js 프로젝트
- ✅ npm 패키지 대부분이 CommonJS
- ✅ 동적 로딩이 많이 필요한 경우
- ✅ 빠른 프로토타이핑 ES Modules 사용:
- ✅ 새 프로젝트
- ✅ 브라우저와 코드 공유
- ✅ 트리 쉐이킹 필요 (번들 크기 최적화)
- ✅ 정적 분석 도구 활용
혼용 (상호 운용성)
CommonJS에서 ES Modules 사용:
// CommonJS 파일
async function loadESModule() {
const module = await import('./es-module.mjs');
module.default();
}
ES Modules에서 CommonJS 사용:
// ES Modules 파일
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const commonjsModule = require('./commonjs-module.js');
4. 내장 모듈
주요 내장 모듈
// 파일 시스템
const fs = require('fs');
const fsPromises = require('fs').promises;
// 경로 처리
const path = require('path');
// HTTP/HTTPS
const http = require('http');
const https = require('https');
// URL 처리
const url = require('url');
// 쿼리스트링
const querystring = require('querystring');
// 운영체제 정보
const os = require('os');
// 암호화
const crypto = require('crypto');
// 이벤트
const EventEmitter = require('events');
// 스트림
const stream = require('stream');
// 자식 프로세스
const child_process = require('child_process');
fs (파일 시스템)
const fs = require('fs').promises;
const path = require('path');
async function fileOperations() {
try {
// 파일 읽기
const data = await fs.readFile('input.txt', 'utf8');
console.log('파일 내용:', data);
// 파일 쓰기
await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
// 파일 추가
await fs.appendFile('output.txt', '\n추가 내용', 'utf8');
// 파일 복사
await fs.copyFile('output.txt', 'backup.txt');
// 파일 이름 변경
await fs.rename('backup.txt', 'backup-new.txt');
// 파일 삭제
await fs.unlink('backup-new.txt');
// 파일 정보
const stats = await fs.stat('output.txt');
console.log('파일 크기:', stats.size);
console.log('생성 시간:', stats.birthtime);
console.log('수정 시간:', stats.mtime);
// 디렉토리 생성
await fs.mkdir('new-folder', { recursive: true });
// 디렉토리 읽기
const files = await fs.readdir('.');
console.log('파일 목록:', files);
// 디렉토리 삭제
await fs.rmdir('new-folder');
} catch (err) {
console.error('에러:', err.message);
}
}
fileOperations();
path (경로 처리)
const path = require('path');
// 경로 결합
const filePath = path.join(__dirname, 'data', 'users.json');
console.log(filePath);
// C:\Users\JB\workspace\pkglog.com\data\users.json
// 절대 경로 생성
const absolutePath = path.resolve('data', 'users.json');
console.log(absolutePath);
// 파일 이름
console.log(path.basename('/foo/bar/file.txt')); // file.txt
console.log(path.basename('/foo/bar/file.txt', '.txt')); // file
// 디렉토리 이름
console.log(path.dirname('/foo/bar/file.txt')); // /foo/bar
// 확장자
console.log(path.extname('file.txt')); // .txt
// 경로 파싱
const parsed = path.parse('/foo/bar/file.txt');
console.log(parsed);
// {
// root: '/',
// dir: '/foo/bar',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }
// 경로 정규화
console.log(path.normalize('/foo/bar/../baz')); // /foo/baz
// 상대 경로
console.log(path.relative('/foo/bar', '/foo/baz/file.txt'));
// ../baz/file.txt
os (운영체제 정보)
const os = require('os');
// 플랫폼
console.log('플랫폼:', os.platform()); // win32, darwin, linux
// CPU 정보
console.log('CPU:', os.cpus().length, '코어');
// 메모리
console.log('총 메모리:', (os.totalmem() / 1024 / 1024 / 1024).toFixed(2), 'GB');
console.log('여유 메모리:', (os.freemem() / 1024 / 1024 / 1024).toFixed(2), 'GB');
// 홈 디렉토리
console.log('홈:', os.homedir());
// 임시 디렉토리
console.log('임시:', os.tmpdir());
// 네트워크 인터페이스
console.log('네트워크:', os.networkInterfaces());
crypto (암호화)
const crypto = require('crypto');
// 해시 생성
function hashPassword(password) {
return crypto
.createHash('sha256')
.update(password)
.digest('hex');
}
console.log(hashPassword('mypassword'));
// 89e01536ac207279409d4de1e5253e01f4a1769e696db0d6062ca9b8f56767c8
// 랜덤 문자열
const randomString = crypto.randomBytes(16).toString('hex');
console.log(randomString);
// a3f5c8b2e9d1f4a7c6b8e2d9f1a4c7b6
// UUID
const { randomUUID } = require('crypto');
console.log(randomUUID());
// 550e8400-e29b-41d4-a716-446655440000
5. 모듈 캐싱
캐싱 동작
// counter.js
let count = 0;
exports.increment = () => {
count++;
console.log('Count:', count);
};
exports.getCount = () => count;
// app.js
const counter1 = require('./counter');
const counter2 = require('./counter');
counter1.increment(); // Count: 1
counter2.increment(); // Count: 2
console.log(counter1.getCount()); // 2
console.log(counter2.getCount()); // 2
// counter1과 counter2는 같은 인스턴스!
console.log(counter1 === counter2); // true
설명
- Node.js는 모듈을 처음 로드할 때 캐싱
- 같은 모듈을 여러 번
require해도 한 번만 실행 - 모든
require는 같은 인스턴스 반환
캐시 확인 및 삭제
// 캐시된 모듈 확인
console.log(require.cache);
// 캐시 삭제 (테스트 용도)
delete require.cache[require.resolve('./counter')];
// 다시 로드하면 새 인스턴스
const counter3 = require('./counter');
counter3.increment(); // Count: 1 (새로 시작)
6. 순환 참조 (Circular Dependency)
문제 상황
// a.js
const b = require('./b');
exports.name = 'Module A';
exports.greet = () => {
console.log(`A: ${b.name}`);
};
// b.js
// 변수 선언 및 초기화
const a = require('./a');
exports.name = 'Module B';
exports.greet = () => {
console.log(`B: ${a.name}`);
};
// app.js
// 변수 선언 및 초기화
const a = require('./a');
const b = require('./b');
a.greet(); // A: Module B
b.greet(); // B: undefined (순환 참조!)
문제: b.js가 a.js를 로드할 때, a.js는 아직 완전히 로드되지 않음.
해결 방법
방법 1: 구조 재설계 (권장)
// shared.js
exports.nameA = 'Module A';
exports.nameB = 'Module B';
// a.js
const shared = require('./shared');
exports.greet = () => {
console.log(`A: ${shared.nameB}`);
};
// b.js
// 변수 선언 및 초기화
const shared = require('./shared');
exports.greet = () => {
console.log(`B: ${shared.nameA}`);
};
방법 2: 지연 로딩 (Lazy Loading)
// b.js
exports.name = 'Module B';
exports.greet = () => {
// 함수 실행 시점에 로드
const a = require('./a');
console.log(`B: ${a.name}`);
};
방법 3: 의존성 주입
// a.js
exports.name = 'Module A';
exports.setB = (b) => {
exports.b = b;
};
exports.greet = () => {
console.log(`A: ${exports.b.name}`);
};
// app.js
const a = require('./a');
const b = require('./b');
a.setB(b);
b.setA(a);
a.greet(); // A: Module B
b.greet(); // B: Module A
7. 모듈 해석 (Module Resolution)
모듈 경로 규칙
// 1. 상대 경로
require('./math'); // 같은 폴더
require('../utils/math'); // 상위 폴더
require('./lib/math'); // 하위 폴더
// 2. 절대 경로
require('/home/user/project/math');
// 3. 패키지 이름 (node_modules)
require('express');
require('lodash');
// 4. 내장 모듈
require('fs');
require('http');
파일 확장자 생략
// 다음 순서로 검색
require('./math');
// 1. ./math.js
// 2. ./math.json
// 3. ./math.node (네이티브 모듈)
// 4. ./math/index.js
// 5. ./math/package.json의 "main" 필드
node_modules 검색
// require('express') 실행 시 검색 순서
// 1. ./node_modules/express
// 2. ../node_modules/express
// 3. ../../node_modules/express
// ... (루트까지 계속)
확인:
console.log(require.resolve('express'));
// /home/user/project/node_modules/express/index.js
console.log(require.resolve.paths('express'));
// [ '/home/user/project/node_modules',
// '/home/user/node_modules',
// '/home/node_modules',
// '/node_modules' ]
8. 실전 예제
예제 1: 설정 모듈
// config.js
require('dotenv').config();
const config = {
server: {
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost'
},
database: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || 'mydb',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || '
},
jwt: {
secret: process.env.JWT_SECRET || 'default-secret',
expiresIn: '1h'
},
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production'
};
module.exports = config;
// server.js
const config = require('./config');
console.log(`서버 포트: ${config.server.port}`);
console.log(`환경: ${config.isDevelopment ? '개발' : '운영'}`);
예제 2: 로거 모듈
// logger.js
const fs = require('fs');
const path = require('path');
class Logger {
constructor(logFile) {
this.logFile = logFile;
}
_write(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
// 콘솔 출력
console.log(logMessage.trim());
// 파일 저장
fs.appendFileSync(this.logFile, logMessage, 'utf8');
}
info(message) {
this._write('INFO', message);
}
error(message) {
this._write('ERROR', message);
}
warn(message) {
this._write('WARN', message);
}
debug(message) {
if (process.env.NODE_ENV === 'development') {
this._write('DEBUG', message);
}
}
}
// 싱글톤 인스턴스
const logger = new Logger(path.join(__dirname, 'app.log'));
module.exports = logger;
// app.js
const logger = require('./logger');
logger.info('서버 시작');
logger.error('데이터베이스 연결 실패');
logger.warn('메모리 사용량 높음');
logger.debug('디버그 정보');
예제 3: 데이터베이스 모듈
// database.js
class Database {
constructor() {
this.connection = null;
this.connected = false;
}
async connect(config) {
if (this.connected) {
console.log('이미 연결됨');
return this.connection;
}
try {
// 실제로는 데이터베이스 연결 로직
this.connection = {
host: config.host,
port: config.port,
database: config.database
};
this.connected = true;
console.log(`데이터베이스 연결 성공: ${config.host}:${config.port}`);
return this.connection;
} catch (err) {
console.error('데이터베이스 연결 실패:', err.message);
throw err;
}
}
async query(sql, params = []) {
if (!this.connected) {
throw new Error('데이터베이스에 연결되지 않음');
}
console.log('쿼리 실행:', sql, params);
// 실제 쿼리 실행 로직
return [];
}
async close() {
if (this.connected) {
this.connection = null;
this.connected = false;
console.log('데이터베이스 연결 종료');
}
}
}
// 싱글톤
module.exports = new Database();
// app.js
const db = require('./database');
const config = require('./config');
async function main() {
try {
await db.connect(config.database);
const users = await db.query('SELECT * FROM users WHERE age > ?', [18]);
console.log('사용자:', users);
await db.close();
} catch (err) {
console.error('에러:', err.message);
}
}
main();
예제 4: API 클라이언트 모듈
// api-client.js
const https = require('https');
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
request(path, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(path, this.baseUrl);
const req = https.request(url, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
}
}, (res) => {
let data = ';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve(json);
} catch (err) {
reject(err);
}
});
});
req.on('error', reject);
if (options.body) {
req.write(JSON.stringify(options.body));
}
req.end();
});
}
get(path) {
return this.request(path, { method: 'GET' });
}
post(path, body) {
return this.request(path, { method: 'POST', body });
}
}
module.exports = ApiClient;
// app.js
const ApiClient = require('./api-client');
const client = new ApiClient('https://api.github.com');
async function fetchUser(username) {
try {
const user = await client.get(`/users/${username}`);
console.log('사용자:', user.name);
console.log('저장소:', user.public_repos);
} catch (err) {
console.error('에러:', err.message);
}
}
fetchUser('torvalds');
9. package.json 심화
필수 필드
{
"name": "my-package",
"version": "1.0.0",
"description": "패키지 설명",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "jest"
},
"keywords": ["node", "javascript"],
"author": "Your Name <your@email.com>",
"license": "MIT"
}
dependencies vs devDependencies
{
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.0.0"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.5.0",
"eslint": "^8.50.0"
}
}
차이
dependencies: 프로덕션에서 필요한 패키지devDependencies: 개발 중에만 필요한 패키지
# 프로덕션 설치 (devDependencies 제외)
npm install --production
버전 관리 (Semantic Versioning)
{
"dependencies": {
"express": "^4.18.2"
}
}
버전 형식: MAJOR.MINOR.PATCH
4.18.2: 정확히 4.18.2만^4.18.2: 4.18.2 이상, 5.0.0 미만 (MINOR, PATCH 업데이트 허용)~4.18.2: 4.18.2 이상, 4.19.0 미만 (PATCH만 업데이트 허용)*: 최신 버전>=4.18.2: 4.18.2 이상
scripts 활용
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"build": "webpack --mode production",
"clean": "rm -rf dist",
"prebuild": "npm run clean",
"postbuild": "echo 'Build complete!'",
"deploy": "npm run build && npm run upload"
}
}
실행 순서:
npm run build
# 1. prebuild 실행
# 2. build 실행
# 3. postbuild 실행
10. 모듈 패턴
싱글톤 패턴
// database.js
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
if (!this.connection) {
this.connection = { connected: true };
console.log('연결됨');
}
}
}
module.exports = new Database();
팩토리 패턴
// user-factory.js
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
}
class Admin extends User {
constructor(name) {
super(name, 'admin');
this.permissions = ['read', 'write', 'delete'];
}
}
class Guest extends User {
constructor(name) {
super(name, 'guest');
this.permissions = ['read'];
}
}
function createUser(name, type) {
switch (type) {
case 'admin':
return new Admin(name);
case 'guest':
return new Guest(name);
default:
return new User(name, 'user');
}
}
module.exports = { createUser };
모듈 패턴 (Private 변수)
// counter.js
const counter = (() => {
// Private 변수
let count = 0;
// Public API
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
},
reset() {
count = 0;
}
};
})();
module.exports = counter;
11. 자주 발생하는 문제
문제 1: Cannot find module
에러:
Error: Cannot find module './math'
원인
- 파일 경로 오타
- 확장자 누락 (
.mjs는 명시 필요) - 패키지 미설치 해결:
// ✅ 상대 경로 확인
require('./math'); // math.js가 같은 폴더에 있어야 함
// ✅ 절대 경로 사용
const path = require('path');
require(path.join(__dirname, 'math'));
// ✅ 패키지 설치
npm install express
문제 2: ES Modules 오류
에러:
SyntaxError: Cannot use import statement outside a module
해결:
// package.json
{
"type": "module"
}
또는 .mjs 확장자 사용.
문제 3: __dirname, __filename 없음 (ES Modules)
문제:
// ES Modules에서는 __dirname, __filename 없음
console.log(__dirname); // ReferenceError
해결:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
console.log(__filename);
문제 4: 순환 참조
증상: 모듈이 undefined 또는 일부만 로드됨
해결: 위 “순환 참조” 섹션 참조
12. 실전 팁
모듈 구조화
src/
├── config/
│ ├── database.js
│ ├── server.js
│ └── index.js
├── models/
│ ├── user.js
│ └── post.js
├── controllers/
│ ├── userController.js
│ └── postController.js
├── routes/
│ ├── userRoutes.js
│ └── postRoutes.js
├── middlewares/
│ ├── auth.js
│ └── errorHandler.js
├── utils/
│ ├── logger.js
│ └── validator.js
└── index.js
index.js 패턴
// models/index.js
const User = require('./user');
const Post = require('./post');
const Comment = require('./comment');
module.exports = {
User,
Post,
Comment
};
// app.js
const { User, Post } = require('./models');
const user = new User('홍길동');
const post = new Post('제목');
환경별 설정
// config/index.js
const development = require('./development');
const production = require('./production');
const test = require('./test');
const configs = {
development,
production,
test
};
const env = process.env.NODE_ENV || 'development';
module.exports = configs[env];
에러 처리
// utils/errors.js
// 타입 정의
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = '리소스를 찾을 수 없습니다') {
super(message, 404);
}
}
class ValidationError extends AppError {
constructor(message = '유효하지 않은 입력입니다') {
super(message, 400);
}
}
module.exports = {
AppError,
NotFoundError,
ValidationError
};
정리
핵심 요약
- CommonJS:
require,module.exports(Node.js 기본) - ES Modules:
import,export(표준, 최신) - 내장 모듈:
fs,path,http,os,crypto - 모듈 캐싱: 한 번 로드하면 캐시됨
- 순환 참조: 구조 재설계 또는 지연 로딩으로 해결
- package.json: 프로젝트 메타데이터, 의존성 관리
비교: CommonJS vs ES Modules
| 특징 | CommonJS | ES Modules |
|---|---|---|
| 문법 | require/module.exports | import/export |
| 로딩 | 동기, 런타임 | 비동기, 정적 |
| 동적 로딩 | 기본 지원 | import() 사용 |
| 트리 쉐이킹 | 불가 | 가능 |
| 브라우저 | 불가 | 가능 |
다음 단계
추천 학습 자료
공식 문서
-
ECMAScript Modules 패키지 검색
-
npms.io (패키지 품질 평가)
관련 글
- JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리
- C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
- JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문
- Node.js 시작하기 | 설치, 설정, Hello World
- Node.js 비동기 프로그래밍 | Callback, Promise, Async/Await
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Node.js 모듈 시스템: CommonJS와 ES Modules 완벽 가이드. CommonJS 모듈·ES Modules로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Node.js·JavaScript·모… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Makefile 완벽 가이드 | 10분만에 배우는 빌드 자동화 (실전 예제)
- JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리
- C++ 링킹 완벽 가이드 | 정적/동적 링킹과 에러 해결 (undefined reference)
이 글에서 다루는 키워드 (관련 검색어)
Node.js, JavaScript, 모듈, CommonJS, ES Modules, require, import 등으로 검색하시면 이 글이 도움이 됩니다.