Node.js 파일 시스템 | fs 모듈 완벽 가이드
이 글의 핵심
Node.js 파일 시스템에 대한 실전 가이드입니다. fs 모듈 완벽 가이드 등을 예제와 함께 상세히 설명합니다.
들어가며
fs 모듈이란?
fs (File System) 모듈은 Node.js에서 파일과 디렉토리를 다루는 내장 모듈입니다.
디스크에서 읽고 쓰는 일은 네트워크와 마찬가지로 시간이 걸리는 I/O라서, 서버 코드에서는 readFile 같은 비동기 API를 쓰는 것이 기본입니다. 동기(readFileSync)는 스레드 전체가 멈춘 것처럼 다음 요청을 못 받을 수 있으니, 초기화 스크립트나 CLI처럼 짧게 쓸 때만 쓰는 편이 안전합니다.
주요 기능:
- ✅ 파일 읽기/쓰기
- ✅ 디렉토리 생성/삭제
- ✅ 파일 정보 조회
- ✅ 파일 감시 (watch)
- ✅ 스트림 처리
세 가지 API 스타일:
- 동기 (Sync): 작업이 끝날 때까지 대기
- 비동기 (Callback): 콜백 함수로 결과 처리
- 비동기 (Promise): Promise로 결과 처리 (권장)
1. 파일 읽기
동기 방식
const fs = require('fs');
try {
const data = fs.readFileSync('file.txt', 'utf8');
console.log('파일 내용:', data);
} catch (err) {
console.error('에러:', err.message);
}
console.log('다음 코드');
// 출력 순서: 파일 내용 → 다음 코드
주의: 동기 방식은 서버를 블로킹하므로 서버 코드에서는 사용 금지!
비동기 (Callback)
const fs = require('fs');
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('에러:', err.message);
return;
}
console.log('파일 내용:', data);
});
console.log('다음 코드');
// 출력 순서: 다음 코드 → 파일 내용
비동기 (Promise) - 권장
Promise 기반 API는 async/await와 함께 사용하여 가장 깔끔한 코드를 작성할 수 있습니다:
// fs.promises: Promise 기반 API
const fs = require('fs').promises;
async function readFile() {
try {
// await: Promise가 완료될 때까지 대기
// 코드는 동기처럼 보이지만 실제로는 비동기
// 다른 작업을 블로킹하지 않음
const data = await fs.readFile('file.txt', 'utf8');
// 'utf8': 텍스트 인코딩 지정
// 생략하면 Buffer 객체 반환
console.log('파일 내용:', data);
// data: 파일의 텍스트 내용 (string)
} catch (err) {
// 파일이 없거나 권한 문제 등 에러 발생 시
console.error('에러:', err.message);
// 에러 코드로 세부 처리
if (err.code === 'ENOENT') {
console.error('파일이 존재하지 않습니다');
} else if (err.code === 'EACCES') {
console.error('파일 접근 권한이 없습니다');
}
}
}
// async 함수 호출
readFile();
// 함수가 즉시 반환되고 파일 읽기는 백그라운드에서 진행
console.log('다음 코드');
// 출력 순서: "다음 코드" → "파일 내용: ..."
여러 파일 순차 읽기:
async function readMultipleFiles() {
try {
// 순차 실행 (하나씩)
const file1 = await fs.readFile('file1.txt', 'utf8');
const file2 = await fs.readFile('file2.txt', 'utf8');
const file3 = await fs.readFile('file3.txt', 'utf8');
console.log('모든 파일 읽기 완료');
} catch (err) {
console.error('에러:', err.message);
}
}
여러 파일 병렬 읽기 (더 빠름):
async function readFilesParallel() {
try {
// Promise.all: 모든 Promise를 동시에 실행
const [file1, file2, file3] = await Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('file3.txt', 'utf8')
]);
// 3개 파일을 동시에 읽음 (병렬)
// 가장 느린 파일이 끝나면 모두 완료
console.log('모든 파일 읽기 완료');
} catch (err) {
// 하나라도 실패하면 전체 실패
console.error('에러:', err.message);
}
}
인코딩
// 텍스트 파일 (UTF-8)
const text = await fs.readFile('text.txt', 'utf8');
// 바이너리 파일 (Buffer)
const buffer = await fs.readFile('image.png');
console.log(buffer); // <Buffer 89 50 4e 47 ...>
// Buffer를 문자열로 변환
const str = buffer.toString('utf8');
// Buffer를 Base64로 변환
const base64 = buffer.toString('base64');
2. 파일 쓰기
기본 쓰기
const fs = require('fs').promises;
async function writeFile() {
try {
// 파일 쓰기 (덮어쓰기)
await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
console.log('파일 쓰기 완료');
// 파일 추가 (append)
await fs.appendFile('output.txt', '\n추가 내용', 'utf8');
console.log('내용 추가 완료');
} catch (err) {
console.error('에러:', err.message);
}
}
writeFile();
JSON 파일 처리
JSON 파일을 읽고 쓰는 유틸리티 함수입니다:
const fs = require('fs').promises;
// JSON 읽기 함수
async function readJSON(filename) {
try {
// 1. 파일을 텍스트로 읽기
const data = await fs.readFile(filename, 'utf8');
// data: JSON 형식의 문자열
// 2. JSON 문자열을 JavaScript 객체로 변환
return JSON.parse(data);
// JSON.parse(): 문자열 → 객체
} catch (err) {
// 에러 코드별 처리
if (err.code === 'ENOENT') {
// ENOENT: Error NO ENTry (파일 없음)
return null; // 파일이 없으면 null 반환
}
// 다른 에러는 상위로 전파
throw err;
}
}
// JSON 쓰기 함수
async function writeJSON(filename, data) {
// 1. JavaScript 객체를 JSON 문자열로 변환
const json = JSON.stringify(data, null, 2);
// JSON.stringify(값, replacer, 들여쓰기)
// null: replacer 없음 (모든 속성 포함)
// 2: 들여쓰기 2칸 (가독성 향상)
// 2. JSON 문자열을 파일에 쓰기
await fs.writeFile(filename, json, 'utf8');
}
// 사용 예제
async function main() {
// 1. 데이터 준비
const users = [
{ id: 1, name: '홍길동', age: 25 },
{ id: 2, name: '김철수', age: 30 }
];
// 2. JSON 파일로 저장
await writeJSON('users.json', users);
console.log('users.json 저장 완료');
// 저장된 파일 내용:
// [
// {
// "id": 1,
// "name": "홍길동",
// "age": 25
// },
// {
// "id": 2,
// "name": "김철수",
// "age": 30
// }
// ]
// 3. JSON 파일 읽기
const loadedUsers = await readJSON('users.json');
console.log(loadedUsers);
// [
// { id: 1, name: '홍길동', age: 25 },
// { id: 2, name: '김철수', age: 30 }
// ]
// 4. 데이터 수정 후 다시 저장
loadedUsers[0].age = 26;
await writeJSON('users.json', loadedUsers);
console.log('users.json 업데이트 완료');
}
main().catch(console.error);
// catch: main 함수에서 발생한 에러 처리
실전 예시: 설정 파일 관리:
// config.json 읽기/쓰기
const CONFIG_FILE = 'config.json';
async function loadConfig() {
const config = await readJSON(CONFIG_FILE);
// 파일이 없으면 기본 설정 반환
return config || {
port: 3000,
host: 'localhost',
debug: false
};
}
async function saveConfig(config) {
await writeJSON(CONFIG_FILE, config);
}
// 사용
const config = await loadConfig();
config.port = 8080; // 포트 변경
await saveConfig(config); // 저장
3. 파일 정보 및 관리
파일 정보 (stat)
const fs = require('fs').promises;
async function fileInfo(filename) {
try {
const stats = await fs.stat(filename);
console.log('파일 크기:', stats.size, 'bytes');
console.log('생성 시간:', stats.birthtime);
console.log('수정 시간:', stats.mtime);
console.log('접근 시간:', stats.atime);
console.log('파일인가?', stats.isFile());
console.log('디렉토리인가?', stats.isDirectory());
console.log('심볼릭 링크인가?', stats.isSymbolicLink());
} catch (err) {
console.error('에러:', err.message);
}
}
fileInfo('file.txt');
파일 존재 확인
const fs = require('fs').promises;
async function fileExists(filename) {
try {
await fs.access(filename);
return true;
} catch {
return false;
}
}
// 사용
if (await fileExists('file.txt')) {
console.log('파일 존재');
} else {
console.log('파일 없음');
}
파일 작업
const fs = require('fs').promises;
async function fileOperations() {
try {
// 파일 복사
await fs.copyFile('source.txt', 'destination.txt');
console.log('파일 복사 완료');
// 파일 이름 변경 / 이동
await fs.rename('old-name.txt', 'new-name.txt');
console.log('파일 이름 변경 완료');
// 파일 삭제
await fs.unlink('file-to-delete.txt');
console.log('파일 삭제 완료');
// 파일 권한 변경 (Linux/macOS)
await fs.chmod('file.txt', 0o755);
} catch (err) {
console.error('에러:', err.message);
}
}
fileOperations();
4. 디렉토리 관리
디렉토리 생성
const fs = require('fs').promises;
const path = require('path');
async function createDirectories() {
try {
// 단일 디렉토리
await fs.mkdir('new-folder');
// 중첩 디렉토리 (recursive)
await fs.mkdir('parent/child/grandchild', { recursive: true });
console.log('디렉토리 생성 완료');
} catch (err) {
if (err.code === 'EEXIST') {
console.log('디렉토리가 이미 존재합니다');
} else {
console.error('에러:', err.message);
}
}
}
createDirectories();
디렉토리 읽기
const fs = require('fs').promises;
const path = require('path');
async function listFiles(directory) {
try {
const files = await fs.readdir(directory);
console.log(`${directory} 내용:`);
for (const file of files) {
console.log(`- ${file}`);
}
} catch (err) {
console.error('에러:', err.message);
}
}
// 상세 정보 포함
async function listFilesDetailed(directory) {
try {
const files = await fs.readdir(directory, { withFileTypes: true });
for (const file of files) {
const type = file.isDirectory() ? '[DIR]' : '[FILE]';
console.log(`${type} ${file.name}`);
}
} catch (err) {
console.error('에러:', err.message);
}
}
listFiles('.');
listFilesDetailed('.');
재귀적 디렉토리 탐색
const fs = require('fs').promises;
const path = require('path');
async function walkDirectory(directory, callback) {
const files = await fs.readdir(directory, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(directory, file.name);
if (file.isDirectory()) {
await walkDirectory(fullPath, callback);
} else {
await callback(fullPath);
}
}
}
// 사용: 모든 .js 파일 찾기
async function findJSFiles(directory) {
const jsFiles = [];
await walkDirectory(directory, async (filePath) => {
if (path.extname(filePath) === '.js') {
jsFiles.push(filePath);
}
});
return jsFiles;
}
// 실행
findJSFiles('./src').then(files => {
console.log('JS 파일:', files);
});
디렉토리 삭제
const fs = require('fs').promises;
async function removeDirectory() {
try {
// 빈 디렉토리 삭제
await fs.rmdir('empty-folder');
// 디렉토리와 내용 모두 삭제 (recursive)
await fs.rm('folder-with-files', { recursive: true, force: true });
console.log('디렉토리 삭제 완료');
} catch (err) {
console.error('에러:', err.message);
}
}
removeDirectory();
5. 스트림 (Stream)
읽기 스트림
const fs = require('fs');
const readStream = fs.createReadStream('large-file.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB 청크
});
let totalSize = 0;
readStream.on('data', (chunk) => {
totalSize += chunk.length;
console.log(`청크 받음: ${chunk.length} bytes`);
});
readStream.on('end', () => {
console.log(`총 ${totalSize} bytes 읽음`);
});
readStream.on('error', (err) => {
console.error('에러:', err.message);
});
쓰기 스트림
const fs = require('fs');
const writeStream = fs.createWriteStream('output.txt');
writeStream.write('첫 번째 줄\n');
writeStream.write('두 번째 줄\n');
writeStream.write('세 번째 줄\n');
writeStream.end('마지막 줄\n');
writeStream.on('finish', () => {
console.log('파일 쓰기 완료');
});
writeStream.on('error', (err) => {
console.error('에러:', err.message);
});
파이프 (Pipe)
const fs = require('fs');
// 파일 복사
fs.createReadStream('input.txt')
.pipe(fs.createWriteStream('output.txt'));
// 진행률 표시
const readStream = fs.createReadStream('large-file.txt');
const writeStream = fs.createWriteStream('copy.txt');
let totalSize = 0;
readStream.on('data', (chunk) => {
totalSize += chunk.length;
process.stdout.write(`\r복사 중: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
});
readStream.on('end', () => {
console.log('\n복사 완료');
});
readStream.pipe(writeStream);
압축
const fs = require('fs');
const zlib = require('zlib');
// 파일 압축
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
// 파일 압축 해제
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('output.txt'));
// 여러 변환 체이닝
const { Transform } = require('stream');
class UpperCaseTransform extends Transform {
_transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
}
fs.createReadStream('input.txt')
.pipe(new UpperCaseTransform())
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('output.txt.gz'));
6. 파일 감시 (Watch)
fs.watch
const fs = require('fs');
const watcher = fs.watch('watched-folder', { recursive: true }, (eventType, filename) => {
console.log(`이벤트: ${eventType}`);
console.log(`파일: ${filename}`);
});
// 감시 중지
setTimeout(() => {
watcher.close();
console.log('감시 중지');
}, 60000);
fs.watchFile (폴링 방식)
const fs = require('fs');
fs.watchFile('file.txt', { interval: 1000 }, (curr, prev) => {
console.log('파일 변경됨');
console.log('이전 수정 시간:', prev.mtime);
console.log('현재 수정 시간:', curr.mtime);
});
// 감시 중지
setTimeout(() => {
fs.unwatchFile('file.txt');
}, 60000);
chokidar (권장)
npm install chokidar
const chokidar = require('chokidar');
const watcher = chokidar.watch('src/**/*.js', {
ignored: /(^|[\/\\])\../, // 숨김 파일 제외
persistent: true
});
watcher
.on('add', path => console.log(`파일 추가: ${path}`))
.on('change', path => console.log(`파일 변경: ${path}`))
.on('unlink', path => console.log(`파일 삭제: ${path}`))
.on('addDir', path => console.log(`디렉토리 추가: ${path}`))
.on('unlinkDir', path => console.log(`디렉토리 삭제: ${path}`))
.on('error', error => console.error(`에러: ${error}`))
.on('ready', () => console.log('초기 스캔 완료'));
// 감시 중지
setTimeout(() => {
watcher.close();
}, 60000);
7. 실전 예제
예제 1: 파일 백업 시스템
const fs = require('fs').promises;
const path = require('path');
class BackupManager {
constructor(sourceDir, backupDir) {
this.sourceDir = sourceDir;
this.backupDir = backupDir;
}
async backup() {
try {
// 백업 디렉토리 생성
await fs.mkdir(this.backupDir, { recursive: true });
// 타임스탬프
const timestamp = new Date().toISOString().replace(/:/g, '-');
const backupPath = path.join(this.backupDir, `backup-${timestamp}`);
await fs.mkdir(backupPath);
// 파일 복사
const files = await fs.readdir(this.sourceDir);
for (const file of files) {
const sourcePath = path.join(this.sourceDir, file);
const destPath = path.join(backupPath, file);
const stats = await fs.stat(sourcePath);
if (stats.isFile()) {
await fs.copyFile(sourcePath, destPath);
console.log(`백업: ${file}`);
}
}
console.log(`백업 완료: ${backupPath}`);
return backupPath;
} catch (err) {
console.error('백업 실패:', err.message);
throw err;
}
}
async restore(backupPath) {
try {
const files = await fs.readdir(backupPath);
for (const file of files) {
const sourcePath = path.join(backupPath, file);
const destPath = path.join(this.sourceDir, file);
await fs.copyFile(sourcePath, destPath);
console.log(`복원: ${file}`);
}
console.log('복원 완료');
} catch (err) {
console.error('복원 실패:', err.message);
throw err;
}
}
}
// 사용
async function main() {
const manager = new BackupManager('./data', './backups');
// 백업
const backupPath = await manager.backup();
// 복원
// await manager.restore(backupPath);
}
main();
예제 2: 로그 파일 관리
const fs = require('fs');
const path = require('path');
class Logger {
constructor(logDir = './logs') {
this.logDir = logDir;
this.currentLogFile = null;
this.writeStream = null;
this.ensureLogDir();
this.rotateLog();
}
ensureLogDir() {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
rotateLog() {
// 현재 날짜로 로그 파일 생성
const date = new Date().toISOString().split('T')[0];
const logFile = path.join(this.logDir, `app-${date}.log`);
if (this.currentLogFile !== logFile) {
if (this.writeStream) {
this.writeStream.end();
}
this.currentLogFile = logFile;
this.writeStream = fs.createWriteStream(logFile, { flags: 'a' });
}
}
log(level, message) {
this.rotateLog();
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
// 콘솔 출력
console.log(logMessage.trim());
// 파일 저장
this.writeStream.write(logMessage);
}
info(message) {
this.log('INFO', message);
}
error(message) {
this.log('ERROR', message);
}
warn(message) {
this.log('WARN', message);
}
async cleanup(daysToKeep = 7) {
try {
const files = await fs.promises.readdir(this.logDir);
const now = Date.now();
const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
for (const file of files) {
const filePath = path.join(this.logDir, file);
const stats = await fs.promises.stat(filePath);
if (now - stats.mtime.getTime() > maxAge) {
await fs.promises.unlink(filePath);
console.log(`오래된 로그 삭제: ${file}`);
}
}
} catch (err) {
console.error('정리 실패:', err.message);
}
}
}
// 사용
const logger = new Logger('./logs');
logger.info('서버 시작');
logger.error('데이터베이스 연결 실패');
logger.warn('메모리 사용량 높음');
// 7일 이상 된 로그 삭제
logger.cleanup(7);
예제 3: 파일 검색
const fs = require('fs').promises;
const path = require('path');
async function searchFiles(directory, pattern) {
const results = [];
async function search(dir) {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
await search(fullPath);
} else {
// 파일명 패턴 매칭
if (pattern.test(file.name)) {
const stats = await fs.stat(fullPath);
results.push({
path: fullPath,
name: file.name,
size: stats.size,
modified: stats.mtime
});
}
}
}
}
await search(directory);
return results;
}
// 사용: 모든 .md 파일 찾기
async function main() {
const mdFiles = await searchFiles('./src', /\.md$/);
console.log(`${mdFiles.length}개 파일 발견:`);
mdFiles.forEach(file => {
console.log(`- ${file.name} (${file.size} bytes)`);
});
}
main();
예제 4: CSV 파일 처리
const fs = require('fs');
const readline = require('readline');
async function processCSV(filename) {
const fileStream = fs.createReadStream(filename);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
const results = [];
let isFirstLine = true;
let headers = [];
for await (const line of rl) {
if (isFirstLine) {
headers = line.split(',');
isFirstLine = false;
continue;
}
const values = line.split(',');
const obj = {};
headers.forEach((header, index) => {
obj[header.trim()] = values[index]?.trim() || '';
});
results.push(obj);
}
return results;
}
// 사용
async function main() {
const data = await processCSV('users.csv');
console.log('데이터:', data);
// [
// { name: '홍길동', age: '25', email: '[email protected]' },
// { name: '김철수', age: '30', email: '[email protected]' }
// ]
}
main();
8. 에러 처리
에러 코드
const fs = require('fs').promises;
async function handleErrors() {
try {
const data = await fs.readFile('nonexistent.txt', 'utf8');
} catch (err) {
switch (err.code) {
case 'ENOENT':
console.error('파일을 찾을 수 없습니다');
break;
case 'EACCES':
console.error('권한이 없습니다');
break;
case 'EISDIR':
console.error('디렉토리입니다');
break;
case 'ENOTDIR':
console.error('디렉토리가 아닙니다');
break;
case 'EEXIST':
console.error('이미 존재합니다');
break;
default:
console.error('에러:', err.message);
}
}
}
handleErrors();
안전한 파일 작업
const fs = require('fs').promises;
const path = require('path');
async function safeFileOperation(filename, operation) {
const tempFile = `${filename}.tmp`;
try {
// 임시 파일에 작업
await operation(tempFile);
// 성공하면 원본 파일로 이동
await fs.rename(tempFile, filename);
console.log('작업 완료');
} catch (err) {
// 실패하면 임시 파일 삭제
try {
await fs.unlink(tempFile);
} catch {}
console.error('작업 실패:', err.message);
throw err;
}
}
// 사용
safeFileOperation('data.json', async (tempFile) => {
const data = { users: [] };
await fs.writeFile(tempFile, JSON.stringify(data, null, 2), 'utf8');
});
9. 성능 최적화
버퍼 크기 조정
const fs = require('fs');
// 작은 버퍼 (느림)
const stream1 = fs.createReadStream('file.txt', {
highWaterMark: 16 * 1024 // 16KB
});
// 큰 버퍼 (빠름, 메모리 많이 사용)
const stream2 = fs.createReadStream('file.txt', {
highWaterMark: 256 * 1024 // 256KB
});
병렬 처리
const fs = require('fs').promises;
// ❌ 순차 처리 (느림)
async function sequential(files) {
const results = [];
for (const file of files) {
const data = await fs.readFile(file, 'utf8');
results.push(data);
}
return results;
}
// ✅ 병렬 처리 (빠름)
async function parallel(files) {
const promises = files.map(file => fs.readFile(file, 'utf8'));
return Promise.all(promises);
}
// 사용
const files = ['file1.txt', 'file2.txt', 'file3.txt'];
// 순차: 약 3초
await sequential(files);
// 병렬: 약 1초
await parallel(files);
메모리 효율
const fs = require('fs');
// ❌ 전체 파일을 메모리에 로드 (대용량 파일에 부적합)
async function inefficient(filename) {
const data = await fs.promises.readFile(filename, 'utf8');
return data.split('\n').length;
}
// ✅ 스트림 사용 (메모리 효율적)
async function efficient(filename) {
return new Promise((resolve, reject) => {
let lineCount = 0;
const stream = fs.createReadStream(filename);
stream.on('data', (chunk) => {
lineCount += chunk.toString().split('\n').length - 1;
});
stream.on('end', () => {
resolve(lineCount);
});
stream.on('error', reject);
});
}
10. 자주 발생하는 문제
문제 1: 경로 문제
// ❌ 상대 경로 (현재 작업 디렉토리 기준)
fs.readFileSync('file.txt'); // 실행 위치에 따라 다름
// ✅ 절대 경로 (__dirname 사용)
const path = require('path');
const filePath = path.join(__dirname, 'file.txt');
fs.readFileSync(filePath);
문제 2: 인코딩 누락
// ❌ 인코딩 없음 (Buffer 반환)
const data = await fs.readFile('file.txt');
console.log(data); // <Buffer 48 65 6c 6c 6f>
// ✅ 인코딩 지정
const data = await fs.readFile('file.txt', 'utf8');
console.log(data); // "Hello"
문제 3: 동기 함수 사용
// ❌ 동기 함수 (서버 블로킹)
app.get('/file', (req, res) => {
const data = fs.readFileSync('file.txt', 'utf8');
res.send(data);
});
// ✅ 비동기 함수
app.get('/file', async (req, res) => {
try {
const data = await fs.promises.readFile('file.txt', 'utf8');
res.send(data);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
문제 4: 파일 핸들 누수
// ❌ 스트림을 닫지 않음
const stream = fs.createReadStream('file.txt');
// ... 사용 후 닫지 않으면 메모리 누수
// ✅ 명시적으로 닫기
const stream = fs.createReadStream('file.txt');
stream.on('end', () => {
stream.close();
});
// ✅ 또는 pipeline 사용 (자동으로 정리)
const { pipeline } = require('stream');
const util = require('util');
const pipelineAsync = util.promisify(pipeline);
await pipelineAsync(
fs.createReadStream('input.txt'),
fs.createWriteStream('output.txt')
);
11. 실전 팁
파일 존재 확인 패턴
const fs = require('fs').promises;
// 패턴 1: access 사용
async function exists1(filename) {
try {
await fs.access(filename);
return true;
} catch {
return false;
}
}
// 패턴 2: stat 사용
async function exists2(filename) {
try {
await fs.stat(filename);
return true;
} catch {
return false;
}
}
// 패턴 3: EAFP (Easier to Ask for Forgiveness than Permission)
async function readFileSafe(filename) {
try {
return await fs.readFile(filename, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') {
return null; // 파일 없음
}
throw err; // 다른 에러는 재발생
}
}
디렉토리 비우기
const fs = require('fs').promises;
const path = require('path');
async function emptyDirectory(directory) {
try {
const files = await fs.readdir(directory);
await Promise.all(
files.map(async (file) => {
const filePath = path.join(directory, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await fs.rm(filePath, { recursive: true });
} else {
await fs.unlink(filePath);
}
})
);
console.log('디렉토리 비우기 완료');
} catch (err) {
console.error('에러:', err.message);
}
}
emptyDirectory('./temp');
파일 크기 확인
const fs = require('fs').promises;
const path = require('path');
async function getDirectorySize(directory) {
let totalSize = 0;
async function calculate(dir) {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
await calculate(fullPath);
} else {
const stats = await fs.stat(fullPath);
totalSize += stats.size;
}
}
}
await calculate(directory);
return totalSize;
}
// 사용
async function main() {
const size = await getDirectorySize('./src');
console.log(`디렉토리 크기: ${(size / 1024 / 1024).toFixed(2)} MB`);
}
main();
정리
핵심 요약
- fs 모듈: 파일과 디렉토리 관리
- 세 가지 API: 동기(Sync), 비동기(Callback), 비동기(Promise)
- 권장 방식:
fs.promises사용 (async/await) - 스트림: 대용량 파일 처리, 메모리 효율
- 파일 감시:
fs.watch()또는chokidar - 에러 처리: 에러 코드 확인, try-catch
API 비교
| 작업 | 동기 | 비동기 (Callback) | 비동기 (Promise) |
|---|---|---|---|
| 파일 읽기 | readFileSync | readFile | promises.readFile |
| 파일 쓰기 | writeFileSync | writeFile | promises.writeFile |
| 파일 삭제 | unlinkSync | unlink | promises.unlink |
| 디렉토리 생성 | mkdirSync | mkdir | promises.mkdir |
선택 가이드
동기 방식 사용:
- CLI 도구
- 초기화 코드 (서버 시작 전)
- 간단한 스크립트
비동기 방식 사용 (권장):
- 웹 서버
- API 서버
- 동시 처리가 필요한 경우
스트림 사용:
- 대용량 파일 (100MB+)
- 실시간 처리
- 메모리 제한이 있는 경우
다음 단계
- Node.js 데이터베이스 연동
- Node.js 스트림 심화
- Node.js 테스트
추천 학습 자료
공식 문서:
패키지:
관련 글
- Node.js 시작하기 | 설치, 설정, Hello World