본문으로 건너뛰기
Previous
Next
Node.js 파일 시스템 | fs 모듈 완벽 가이드

Node.js 파일 시스템 | fs 모듈 완벽 가이드

Node.js 파일 시스템 | fs 모듈 완벽 가이드

이 글의 핵심

Node.js 파일 시스템: fs 모듈 파일 읽기·파일 쓰기.

들어가며

fs 모듈이란?

fs (File System) 모듈은 Node.js에서 파일과 디렉토리를 다루는 내장 모듈입니다. 디스크에서 읽고 쓰는 일은 네트워크와 마찬가지로 시간이 걸리는 I/O라서, 서버 코드에서는 readFile 같은 비동기 API를 쓰는 것이 기본입니다. 동기(readFileSync)는 스레드 전체가 멈춘 것처럼 다음 요청을 못 받을 수 있으니, 초기화 스크립트나 CLI처럼 짧게 쓸 때만 쓰는 편이 안전합니다. 주요 기능:

  • ✅ 파일 읽기/쓰기
  • ✅ 디렉토리 생성/삭제
  • ✅ 파일 정보 조회
  • ✅ 파일 감시 (watch)
  • ✅ 스트림 처리 세 가지 API 스타일:
  1. 동기 (Sync): 작업이 끝날 때까지 대기
  2. 비동기 (Callback): 콜백 함수로 결과 처리
  3. 비동기 (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();

정리

핵심 요약

  1. fs 모듈: 파일과 디렉토리 관리
  2. 세 가지 API: 동기(Sync), 비동기(Callback), 비동기(Promise)
  3. 권장 방식: fs.promises 사용 (async/await)
  4. 스트림: 대용량 파일 처리, 메모리 효율
  5. 파일 감시: fs.watch() 또는 chokidar
  6. 에러 처리: 에러 코드 확인, try-catch

API 비교

작업동기비동기 (Callback)비동기 (Promise)
파일 읽기readFileSyncreadFilepromises.readFile
파일 쓰기writeFileSyncwriteFilepromises.writeFile
파일 삭제unlinkSyncunlinkpromises.unlink
디렉토리 생성mkdirSyncmkdirpromises.mkdir

선택 가이드

동기 방식 사용:

  • CLI 도구
  • 초기화 코드 (서버 시작 전)
  • 간단한 스크립트 비동기 방식 사용 (권장):
  • 웹 서버
  • API 서버
  • 동시 처리가 필요한 경우 스트림 사용:
  • 대용량 파일 (100MB+)
  • 실시간 처리
  • 메모리 제한이 있는 경우

다음 단계

추천 학습 자료

공식 문서:


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Node.js 파일 시스템 | fs 모듈 완벽 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Node.js 파일 시스템 | fs 모듈 완벽 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Node.js 파일 시스템: fs 모듈 완벽 가이드. 파일 읽기·파일 쓰기로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Node.js·JavaScript·파일시스템 중심으로 설명합니다. Start now… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Node.js, JavaScript, 파일시스템, fs, 스트림, 파일처리 등으로 검색하시면 이 글이 도움이 됩니다.