Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
이 글의 핵심
Node.js 모듈 시스템에 대한 실전 가이드입니다. 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 모듈
기본 사용법
내보내기 (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');
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('홍길동', '[email protected]');
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
설정
방법 1: package.json
{
"type": "module"
}
방법 2: .mjs 확장자
// math.mjs
export function add(a, b) {
return a + b;
}
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 <[email protected]>",
"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() 사용 |
| 트리 쉐이킹 | 불가 | 가능 |
| 브라우저 | 불가 | 가능 |
다음 단계
- Node.js 비동기 프로그래밍
- Express.js 웹 프레임워크
- Node.js 파일 시스템
추천 학습 자료
공식 문서:
패키지 검색:
관련 글
- JavaScript 모듈 | ES6 Modules, CommonJS 완벽 정리
- C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
- JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문
- Node.js 시작하기 | 설치, 설정, Hello World
- Node.js 비동기 프로그래밍 | Callback, Promise, Async/Await