SQLite — better-sqlite3로 로컬 데이터베이스 구축
"SQLite는 서버 없이 파일 하나로 동작하는 데이터베이스" — Electron 앱에서 로컬 데이터를 영구 저장하려면 SQLite가 가장 실용적인 선택입니다.
설치 및 설정
npm install better-sqlite3
npm install @electron/rebuild --save-dev
npx electron-rebuild
데이터베이스 생성
// 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');
테이블 생성과 마이그레이션
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 구현
// 노트 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 연동
// 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));
트랜잭션 사용
// 여러 작업을 하나의 트랜잭션으로 묶기
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);
}
앱 종료 시 정리
app.on('before-quit', () => {
db.close();
});
면접 포인트 정리
better-sqlite3는 동기 API로 Electron 메인 프로세스에서 사용하기 편리- DB 파일은
app.getPath('userData')에 저장 - WAL 모드는 동시 읽기 성능을 크게 향상시킴
- Prepared Statement를 재사용하면 성능이 좋음
- 트랜잭션으로 여러 쿼리를 원자적으로 실행 가능
- 네이티브 모듈이므로
electron-rebuild필요
SQLite를 다뤘으면, 다음은 electron-store를 이용한 간단한 설정 저장을 알아봅시다.
댓글 로딩 중...