"SQLite는 서버 없이 파일 하나로 동작하는 데이터베이스" — Electron 앱에서 로컬 데이터를 영구 저장하려면 SQLite가 가장 실용적인 선택입니다.


설치 및 설정

BASH
npm install better-sqlite3
npm install @electron/rebuild --save-dev
npx electron-rebuild

데이터베이스 생성

JAVASCRIPT
// main.js — 메인 프로세스에서만 사용
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');

// userData 디렉토리에 DB 파일 생성
const dbPath = path.join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);

// WAL 모드 설정 — 동시 읽기 성능 향상
db.pragma('journal_mode = WAL');

테이블 생성과 마이그레이션

JAVASCRIPT
function initializeDatabase() {
  // 마이그레이션 테이블
  db.exec(`
    CREATE TABLE IF NOT EXISTS migrations (
      id INTEGER PRIMARY KEY,
      name TEXT NOT NULL,
      applied_at TEXT DEFAULT (datetime('now'))
    )
  `);

  const migrations = [
    {
      name: '001_create_notes',
      sql: `
        CREATE TABLE IF NOT EXISTS notes (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          content TEXT DEFAULT '',
          created_at TEXT DEFAULT (datetime('now')),
          updated_at TEXT DEFAULT (datetime('now'))
        )
      `,
    },
    {
      name: '002_add_tags',
      sql: `
        CREATE TABLE IF NOT EXISTS tags (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT UNIQUE NOT NULL
        );
        CREATE TABLE IF NOT EXISTS note_tags (
          note_id INTEGER REFERENCES notes(id) ON DELETE CASCADE,
          tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
          PRIMARY KEY (note_id, tag_id)
        );
      `,
    },
  ];

  const applied = db.prepare('SELECT name FROM migrations').all()
    .map(r => r.name);

  for (const migration of migrations) {
    if (!applied.includes(migration.name)) {
      db.exec(migration.sql);
      db.prepare('INSERT INTO migrations (name) VALUES (?)').run(migration.name);
      console.log(`마이그레이션 적용: ${migration.name}`);
    }
  }
}

CRUD 구현

JAVASCRIPT
// 노트 CRUD 모듈
class NoteRepository {
  constructor(db) {
    this.db = db;
    this.stmts = {
      getAll: db.prepare('SELECT * FROM notes ORDER BY updated_at DESC'),
      getById: db.prepare('SELECT * FROM notes WHERE id = ?'),
      create: db.prepare('INSERT INTO notes (title, content) VALUES (?, ?)'),
      update: db.prepare(`
        UPDATE notes SET title = ?, content = ?, updated_at = datetime('now')
        WHERE id = ?
      `),
      delete: db.prepare('DELETE FROM notes WHERE id = ?'),
      search: db.prepare(`
        SELECT * FROM notes
        WHERE title LIKE ? OR content LIKE ?
        ORDER BY updated_at DESC
      `),
    };
  }

  getAll() {
    return this.stmts.getAll.all();
  }

  getById(id) {
    return this.stmts.getById.get(id);
  }

  create(title, content) {
    const result = this.stmts.create.run(title, content);
    return this.getById(result.lastInsertRowid);
  }

  update(id, title, content) {
    this.stmts.update.run(title, content, id);
    return this.getById(id);
  }

  delete(id) {
    return this.stmts.delete.run(id);
  }

  search(query) {
    const pattern = `%${query}%`;
    return this.stmts.search.all(pattern, pattern);
  }
}

IPC 연동

JAVASCRIPT
// main.js
const noteRepo = new NoteRepository(db);

ipcMain.handle('notes:getAll', () => noteRepo.getAll());
ipcMain.handle('notes:getById', (_e, id) => noteRepo.getById(id));
ipcMain.handle('notes:create', (_e, title, content) => noteRepo.create(title, content));
ipcMain.handle('notes:update', (_e, id, title, content) => noteRepo.update(id, title, content));
ipcMain.handle('notes:delete', (_e, id) => noteRepo.delete(id));
ipcMain.handle('notes:search', (_e, query) => noteRepo.search(query));

트랜잭션 사용

JAVASCRIPT
// 여러 작업을 하나의 트랜잭션으로 묶기
const importNotes = db.transaction((notes) => {
  const insert = db.prepare('INSERT INTO notes (title, content) VALUES (?, ?)');
  for (const note of notes) {
    insert.run(note.title, note.content);
  }
  return notes.length;
});

// 사용: 에러 시 자동 롤백
try {
  const count = importNotes(notesArray);
  console.log(`${count}개 노트 가져오기 완료`);
} catch (error) {
  console.error('가져오기 실패 (자동 롤백됨):', error);
}

앱 종료 시 정리

JAVASCRIPT
app.on('before-quit', () => {
  db.close();
});

면접 포인트 정리

  • better-sqlite3는 동기 API로 Electron 메인 프로세스에서 사용하기 편리
  • DB 파일은 app.getPath('userData')에 저장
  • WAL 모드는 동시 읽기 성능을 크게 향상시킴
  • Prepared Statement를 재사용하면 성능이 좋음
  • 트랜잭션으로 여러 쿼리를 원자적으로 실행 가능
  • 네이티브 모듈이므로 electron-rebuild 필요

SQLite를 다뤘으면, 다음은 electron-store를 이용한 간단한 설정 저장을 알아봅시다.

댓글 로딩 중...