Node.js 핵심 모듈
Node.js는 설치만 하면 바로 사용할 수 있는 **내장 모듈(Built-in Modules)**을 제공합니다. 별도 npm 설치 없이 require()로 불러올 수 있습니다.
fs 모듈: 파일 시스템
fs(File System) 모듈은 파일 읽기, 쓰기, 디렉토리 관리 등 모든 파일 시스템 작업을 담당합니다.
세 가지 스타일
const fs = require('fs');
const fsPromises = require('fs/promises');
// 1. 콜백 스타일 (전통적)
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) {
console.error('읽기 실패:', err.message);
return;
}
console.log('콜백:', data);
});
// 2. 동기 스타일 (블로킹, 스타트업 시에만 권장)
try {
const data = fs.readFileSync('./data.txt', 'utf8');
console.log('동기:', data);
} catch (err) {
console.error('읽기 실패:', err.message);
}
// 3. Promise/async-await 스타일 (권장)
async function readFileAsync() {
try {
const data = await fsPromises.readFile('./data.txt', 'utf8');
console.log('Promise:', data);
} catch (err) {
console.error('읽기 실패:', err.message);
}
}
readFileAsync();
파일 쓰기
const fs = require('fs/promises');
async function writeExample() {
// 새 파일 생성 또는 덮어쓰기
await fs.writeFile('./output.txt', '안녕하세요!\n', 'utf8');
// 파일에 내용 추가
await fs.appendFile('./output.txt', '내용 추가\n', 'utf8');
// 파일 복사
await fs.copyFile('./output.txt', './backup.txt');
// 파일 이름 변경 / 이동
await fs.rename('./output.txt', './renamed.txt');
// 파일 삭제
await fs.unlink('./renamed.txt');
console.log('파일 작업 완료');
}
writeExample();
디렉토리 작업
const fs = require('fs/promises');
const path = require('path');
async function dirExample() {
const dirPath = './test-dir';
// 디렉토리 생성 (중첩 경로도 한 번에)
await fs.mkdir(path.join(dirPath, 'sub', 'nested'), { recursive: true });
console.log('디렉토리 생성 완료');
// 디렉토리 내용 읽기
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const type = entry.isDirectory() ? '디렉토리' : '파일';
console.log(` [${type}] ${entry.name}`);
}
// 파일 정보 확인
const stat = await fs.stat(dirPath);
console.log('생성 시각:', stat.birthtime);
console.log('수정 시각:', stat.mtime);
console.log('디렉토리 여부:', stat.isDirectory());
// 디렉토리 삭제 (recursive로 내부 파일 포함)
await fs.rm(dirPath, { recursive: true, force: true });
console.log('디렉토리 삭제 완료');
}
dirExample();
실전: 디렉토리 파일 목록 재귀 탐색
const fs = require('fs/promises');
const path = require('path');
async function walkDir(dirPath, indent = 0) {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const prefix = ' '.repeat(indent);
if (entry.isDirectory()) {
console.log(`${prefix}📁 ${entry.name}/`);
await walkDir(fullPath, indent + 1);
} else {
const stat = await fs.stat(fullPath);
const size = (stat.size / 1024).toFixed(1);
console.log(`${prefix}📄 ${entry.name} (${size} KB)`);
}
}
}
walkDir('./src').catch(console.error);
path 모듈
경로 문자열을 안전하게 조작합니다. OS마다 경로 구분자가 다르기 때문에(/ vs \) 직접 문자열 처리 대신 path 모듈을 사용해야 합니다.
const path = require('path');
// 경로 결합 (OS 구분자 자동 처리)
console.log(path.join('/users', 'kim', 'documents', 'file.txt'));
// /users/kim/documents/file.txt
// 절대 경로 변환
console.log(path.resolve('./src', 'utils', 'helper.js'));
// /현재작업경로/src/utils/helper.js
// 경로 분해
const filePath = '/home/user/project/src/app.js';
console.log(path.dirname(filePath)); // /home/user/project/src
console.log(path.basename(filePath)); // app.js
console.log(path.extname(filePath)); // .js
console.log(path.basename(filePath, '.js')); // app (확장자 제거)
// 경로 파싱
const parsed = path.parse(filePath);
console.log(parsed);
// { root: '/', dir: '/home/user/project/src', base: 'app.js', ext: '.js', name: 'app' }
// 경로 재조합
console.log(path.format(parsed)); // /home/user/project/src/app.js
// 상대 경로 계산
console.log(path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'));
// ../../impl/bbb
// __dirname과 함께 사용 (현재 파일 기준 경로)
const configPath = path.join(__dirname, '..', 'config', 'app.json');
console.log(configPath);
os 모듈
운영 체제 정보를 가져옵니다. 배포 환경 감지, 리소스 모니터링에 유용합니다.
const os = require('os');
// 기본 정보
console.log('플랫폼:', os.platform()); // linux, win32, darwin
console.log('아키텍처:', os.arch()); // x64, arm64
console.log('OS 버전:', os.version()); // Linux 5.15.0
console.log('호스트명:', os.hostname());
console.log('홈 디렉토리:', os.homedir()); // /home/user
console.log('임시 디렉토리:', os.tmpdir()); // /tmp
// 메모리 정보
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
console.log(`메모리: ${(usedMem / 1e9).toFixed(2)}GB / ${(totalMem / 1e9).toFixed(2)}GB 사용 중`);
// CPU 정보
const cpus = os.cpus();
console.log(`CPU: ${cpus[0].model} x ${cpus.length}코어`);
// 네트워크 인터페이스
const nets = os.networkInterfaces();
for (const [name, addrs] of Object.entries(nets)) {
for (const addr of addrs) {
if (addr.family === 'IPv4' && !addr.internal) {
console.log(`${name}: ${addr.address}`);
}
}
}
// 업타임
const uptime = os.uptime();
const hours = Math.floor(uptime / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
console.log(`시스템 업타임: ${hours}시간 ${minutes}분`);
// 로드 평균 (Linux/macOS만)
if (os.platform() !== 'win32') {
console.log('로드 평균:', os.loadavg());
}
crypto 모듈
암호화 기능을 제공합니다. 해시, HMAC, 랜덤 바이트 생성, 암호화/복호화를 지원합니다.
해시 생성
const crypto = require('crypto');
// MD5 해시 (보안 목적으로는 사용 금지, 체크섬만)
const md5 = crypto.createHash('md5')
.update('Hello, World!')
.digest('hex');
console.log('MD5:', md5);
// SHA-256 (비밀번호 저장에는 bcrypt 권장, 일반 해시용)
const sha256 = crypto.createHash('sha256')
.update('비밀번호123')
.digest('hex');
console.log('SHA-256:', sha256);
// 스트리밍으로 파일 해시 생성
const fs = require('fs');
function hashFile(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
랜덤 값 생성
const crypto = require('crypto');
// 랜덤 바이트 (토큰 생성에 사용)
const token = crypto.randomBytes(32).toString('hex');
console.log('토큰:', token); // 64자 16진수 문자열
// 랜덤 UUID
const uuid = crypto.randomUUID();
console.log('UUID:', uuid); // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// 특정 범위의 랜덤 정수
const randomInt = crypto.randomInt(1, 100);
console.log('랜덤 정수:', randomInt);
HMAC (메시지 인증 코드)
const crypto = require('crypto');
const secretKey = 'my-secret-key';
const message = '{"user":"admin","action":"login"}';
// HMAC 생성
const hmac = crypto.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
console.log('HMAC:', hmac);
// 검증
function verifyHmac(message, signature, key) {
const expected = crypto.createHmac('sha256', key)
.update(message)
.digest('hex');
// 타이밍 공격 방지를 위해 timingSafeEqual 사용
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
console.log('검증:', verifyHmac(message, hmac, secretKey)); // true
stream 모듈 간략 소개
스트림은 데이터를 작은 청크(chunk)로 나눠 처리합니다. 대용량 데이터를 메모리에 전부 올리지 않고 처리할 수 있습니다. (자세한 내용은 streams.md 참조)
const fs = require('fs');
// 파이프: 읽기 스트림 → 쓰기 스트림
const readStream = fs.createReadStream('./large-file.txt');
const writeStream = fs.createWriteStream('./copy.txt');
readStream.pipe(writeStream);
writeStream.on('finish', () => {
console.log('파일 복사 완료');
});
events 모듈: EventEmitter 패턴
Node.js의 많은 내장 모듈이 EventEmitter를 기반으로 만들어져 있습니다. http.Server, fs.ReadStream, net.Socket 모두 EventEmitter를 상속합니다.
기본 사용법
const EventEmitter = require('events');
// EventEmitter 인스턴스 직접 사용
const emitter = new EventEmitter();
// 이벤트 리스너 등록
emitter.on('data', (payload) => {
console.log('데이터 수신:', payload);
});
// 한 번만 실행되는 리스너
emitter.once('connect', () => {
console.log('최초 연결!');
});
// 이벤트 발생
emitter.emit('data', { id: 1, message: '안녕하세요' });
emitter.emit('connect'); // 출력됨
emitter.emit('connect'); // 출력 안 됨 (once)
// 리스너 제거
const handler = (data) => console.log(data);
emitter.on('message', handler);
emitter.off('message', handler); // 제거
EventEmitter 상속 패턴
실제로는 클래스가 EventEmitter를 상속해서 사용하는 패턴이 일반적입니다:
const EventEmitter = require('events');
class Database extends EventEmitter {
constructor(host) {
super();
this.host = host;
this.connected = false;
}
connect() {
// 연결 시뮬레이션
setTimeout(() => {
this.connected = true;
this.emit('connect', { host: this.host });
}, 100);
}
query(sql) {
if (!this.connected) {
this.emit('error', new Error('데이터베이스에 연결되지 않았습니다'));
return;
}
// 쿼리 실행 시뮬레이션
setTimeout(() => {
const result = [{ id: 1, name: '테스트' }];
this.emit('result', { sql, result });
}, 50);
}
disconnect() {
this.connected = false;
this.emit('disconnect');
}
}
// 사용 예
const db = new Database('localhost:5432');
db.on('connect', ({ host }) => {
console.log(`데이터베이스 연결됨: ${host}`);
db.query('SELECT * FROM users');
});
db.on('result', ({ sql, result }) => {
console.log(`쿼리 결과 [${sql}]:`, result);
db.disconnect();
});
db.on('disconnect', () => {
console.log('연결 종료');
});
db.on('error', (err) => {
console.error('에러:', err.message);
});
db.connect();
최대 리스너 경고 처리
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 기본 최대 리스너 수: 10 (초과 시 경고)
emitter.setMaxListeners(20); // 늘리기
// 또는 전역 설정
EventEmitter.defaultMaxListeners = 20;
// 현재 리스너 수 확인
console.log('리스너 수:', emitter.listenerCount('data'));
// 등록된 모든 리스너 이름 확인
console.log('이벤트 목록:', emitter.eventNames());
고수 팁
내장 모듈 vs npm 패키지 선택 기준
// 충분한 경우: 내장 모듈로 해결
const { createHash } = require('crypto'); // bcrypt 대신 학습용으로만
const { join } = require('path'); // path 관련은 내장으로 충분
// npm 패키지가 필요한 경우
// bcrypt: 비밀번호 해싱 (솔트, 반복 횟수 자동 관리)
// sharp: 이미지 처리
// multer: 파일 업로드
util 모듈 활용
const util = require('util');
const fs = require('fs');
// 콜백 함수를 Promise로 변환
const readFilePromise = util.promisify(fs.readFile);
async function main() {
const data = await readFilePromise('./package.json', 'utf8');
console.log(JSON.parse(data).name);
}
main();
// 깊은 객체 출력 (console.log보다 더 자세히)
const complex = { a: { b: { c: { d: 'deep' } } } };
console.log(util.inspect(complex, { depth: null, colors: true }));
// 타입 검사
console.log(util.types.isPromise(Promise.resolve())); // true
console.log(util.types.isGeneratorFunction(function* () {})); // true
모듈 캐싱 이해
// Node.js는 한 번 require한 모듈을 캐싱합니다
const mod1 = require('./config');
const mod2 = require('./config'); // 캐시에서 가져옴, 같은 객체
console.log(mod1 === mod2); // true
// 캐시 강제 무효화 (테스트 환경에서 사용)
delete require.cache[require.resolve('./config')];
const mod3 = require('./config'); // 새로 로드