파일 시스템 접근 — fs와 Node.js API 활용
"Electron의 킬러 기능 중 하나는 로컬 파일 시스템 접근" — 웹에서는 제한적인 파일 조작을 Node.js fs 모듈로 자유롭게 할 수 있습니다.
앱 경로 관리
const { app } = require('electron');
// Electron이 제공하는 표준 경로들
const paths = {
userData: app.getPath('userData'), // 앱 데이터 저장소
documents: app.getPath('documents'), // 사용자 문서 폴더
downloads: app.getPath('downloads'), // 다운로드 폴더
desktop: app.getPath('desktop'), // 바탕화면
temp: app.getPath('temp'), // 임시 폴더
home: app.getPath('home'), // 홈 디렉토리
appData: app.getPath('appData'), // 앱 데이터 루트
logs: app.getPath('logs'), // 로그 폴더
};
userData가 가장 많이 사용됩니다. OS별로 다른 경로를 자동으로 잡아줍니다:
- Windows:
%APPDATA%/앱이름 - macOS:
~/Library/Application Support/앱이름 - Linux:
~/.config/앱이름
파일 읽기/쓰기
메인 프로세스에서 파일 다루기
const fs = require('fs');
const path = require('path');
const { ipcMain, app } = require('electron');
// 파일 읽기
ipcMain.handle('file:read', async (_event, filePath) => {
try {
const content = await fs.promises.readFile(filePath, 'utf-8');
return { success: true, data: content };
} catch (error) {
return { success: false, error: error.message };
}
});
// 파일 쓰기
ipcMain.handle('file:write', async (_event, filePath, content) => {
try {
// 디렉토리가 없으면 자동 생성
const dir = path.dirname(filePath);
await fs.promises.mkdir(dir, { recursive: true });
await fs.promises.writeFile(filePath, content, 'utf-8');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// 파일 존재 여부 확인
ipcMain.handle('file:exists', async (_event, filePath) => {
try {
await fs.promises.access(filePath);
return true;
} catch {
return false;
}
});
디렉토리 읽기
ipcMain.handle('file:readDir', async (_event, dirPath) => {
try {
const entries = await fs.promises.readdir(dirPath, {
withFileTypes: true,
});
return entries.map(entry => ({
name: entry.name,
isDirectory: entry.isDirectory(),
path: path.join(dirPath, entry.name),
}));
} catch (error) {
return [];
}
});
파일 감시 (Watch)
const chokidar = require('chokidar');
// chokidar가 fs.watch보다 안정적입니다
function watchDirectory(dirPath, win) {
const watcher = chokidar.watch(dirPath, {
ignored: /(^|[\/\\])\../, // 숨김 파일 무시
persistent: true,
ignoreInitial: true,
});
watcher.on('add', (filePath) => {
win.webContents.send('file:added', filePath);
});
watcher.on('change', (filePath) => {
win.webContents.send('file:changed', filePath);
});
watcher.on('unlink', (filePath) => {
win.webContents.send('file:removed', filePath);
});
return watcher;
}
앱 설정 파일 관리
// 설정 파일을 userData에 저장하는 패턴
class ConfigManager {
constructor() {
this.configPath = path.join(app.getPath('userData'), 'config.json');
this.config = this.load();
}
load() {
try {
const data = fs.readFileSync(this.configPath, 'utf-8');
return JSON.parse(data);
} catch {
return this.getDefaults();
}
}
save() {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
this.configPath,
JSON.stringify(this.config, null, 2),
'utf-8'
);
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
this.save();
}
getDefaults() {
return {
theme: 'system',
language: 'ko',
autoSave: true,
fontSize: 14,
};
}
}
대용량 파일 처리
const { createReadStream, createWriteStream } = require('fs');
const readline = require('readline');
// 스트림으로 대용량 파일 읽기
ipcMain.handle('file:readLarge', async (_event, filePath) => {
return new Promise((resolve, reject) => {
const lines = [];
const stream = createReadStream(filePath, { encoding: 'utf-8' });
const rl = readline.createInterface({ input: stream });
rl.on('line', (line) => lines.push(line));
rl.on('close', () => resolve(lines));
rl.on('error', reject);
});
});
// 스트림으로 파일 복사 (진행률 포함)
ipcMain.handle('file:copy', async (event, src, dest) => {
const win = BrowserWindow.fromWebContents(event.sender);
const stat = await fs.promises.stat(src);
const totalSize = stat.size;
let copiedSize = 0;
return new Promise((resolve, reject) => {
const readStream = createReadStream(src);
const writeStream = createWriteStream(dest);
readStream.on('data', (chunk) => {
copiedSize += chunk.length;
const progress = Math.round((copiedSize / totalSize) * 100);
win.webContents.send('file:copyProgress', progress);
});
readStream.pipe(writeStream);
writeStream.on('finish', () => resolve(true));
writeStream.on('error', reject);
});
});
보안 주의사항
// 경로 순회 공격 방지
ipcMain.handle('file:read', async (_event, filePath) => {
const allowedDir = app.getPath('userData');
const resolvedPath = path.resolve(filePath);
// 허용된 디렉토리 안인지 확인
if (!resolvedPath.startsWith(allowedDir)) {
throw new Error('접근이 허용되지 않은 경로입니다.');
}
return fs.promises.readFile(resolvedPath, 'utf-8');
});
면접 포인트 정리
app.getPath('userData')가 앱 데이터 저장의 표준 경로- 파일 작업은 반드시 메인 프로세스에서 처리
- 대용량 파일은 스트림 API로 처리해야 메모리 효율적
- 파일 감시는
chokidar가 네이티브fs.watch보다 안정적 - 경로 순회(path traversal) 공격 방지를 위해 경로 검증 필수
파일 시스템을 다뤘으면, 다음은 키보드 단축키 등록 방법을 알아봅시다.
댓글 로딩 중...