Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드

Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드

이 글의 핵심

Node.js 모듈 시스템에 대한 실전 가이드입니다. CommonJS와 ES Modules 완벽 가이드 등을 예제와 함께 상세히 설명합니다.

들어가며

모듈이란?

모듈은 재사용 가능한 코드 조각을 파일로 분리한 것입니다. Node.js는 두 가지 모듈 시스템을 지원합니다:

  1. CommonJS: Node.js 기본 방식 (require, module.exports)
  2. ES Modules: JavaScript 표준 방식 (import, export)

한 프로젝트 안에서 도구를 주머니마다 나눠 담듯 파일을 나누면, 이름이 겹치지 않고(네임스페이스), 테스트와 교체가 쉬워집니다. require('express')처럼 이름만 말하면 node_modules에서 꺼내 오는 것은, 공용 서랍에서 표준 부품을 가져오는 것과 비슷합니다.

npm·package-lock.json의존성 해석·재현 설치의 중심입니다. 같은 축에서 보면 Python pip·uv·Poetry·Go 모듈·go.sum·Rust Cargo가 있고, C++는 CMakeConan·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

exportsmodule.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

비교표

특징CommonJSES Modules
문법require, module.exportsimport, 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.jsa.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
};

정리

핵심 요약

  1. CommonJS: require, module.exports (Node.js 기본)
  2. ES Modules: import, export (표준, 최신)
  3. 내장 모듈: fs, path, http, os, crypto
  4. 모듈 캐싱: 한 번 로드하면 캐시됨
  5. 순환 참조: 구조 재설계 또는 지연 로딩으로 해결
  6. package.json: 프로젝트 메타데이터, 의존성 관리

비교: CommonJS vs ES Modules

특징CommonJSES Modules
문법require/module.exportsimport/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