"Electron의 킬러 기능 중 하나는 로컬 파일 시스템 접근" — 웹에서는 제한적인 파일 조작을 Node.js fs 모듈로 자유롭게 할 수 있습니다.


앱 경로 관리

JAVASCRIPT
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/앱이름

파일 읽기/쓰기

메인 프로세스에서 파일 다루기

JAVASCRIPT
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;
  }
});

디렉토리 읽기

JAVASCRIPT
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)

JAVASCRIPT
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;
}

앱 설정 파일 관리

JAVASCRIPT
// 설정 파일을 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,
    };
  }
}

대용량 파일 처리

JAVASCRIPT
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);
  });
});

보안 주의사항

JAVASCRIPT
// 경로 순회 공격 방지
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) 공격 방지를 위해 경로 검증 필수

파일 시스템을 다뤘으면, 다음은 키보드 단축키 등록 방법을 알아봅시다.

댓글 로딩 중...