본문으로 건너뛰기
← 블로그로 돌아가기
튜토리얼 2026년 3월 8일

SQLite를 로컬 데스크톱 앱 DB로 쓰는 법

서버 없이 SQLite만으로 데스크톱 앱 데이터를 관리하는 실전 패턴.

SQLite Rust Desktop 데이터베이스

왜 SQLite인가

로컬 데스크톱 앱에서 데이터를 저장할 때 선택지는 크게 세 가지다.

  • 파일(JSON/YAML): 단순하지만 데이터가 커지면 느리다
  • SQLite: 관계형 쿼리 가능, 단일 파일, 동시성 처리 내장
  • 외부 DB(PostgreSQL 등): 과하다. 사용자에게 DB 설치를 요구할 수 없다

KeyBox는 시크릿을 수백~수천 개 저장할 수 있어야 했다. 검색, 정렬, 필터링이 필요했고, JSON 파일로는 한계가 있었다. SQLite가 정답이었다.

파일 위치

운영체제별로 앱 데이터 디렉토리가 다르다.

use tauri::api::path::app_data_dir;

fn db_path(app: &tauri::AppHandle) -> PathBuf {
    let dir = app_data_dir(&app.config()).unwrap();
    std::fs::create_dir_all(&dir).ok();
    dir.join("keybox.db")
}
  • Windows: %APPDATA%/com.karnellabs.keybox/
  • macOS: ~/Library/Application Support/com.karnellabs.keybox/
  • Linux: ~/.local/share/com.karnellabs.keybox/

Tauri의 app_data_dir이 OS별 경로를 자동으로 처리한다.

스키마 설계

CREATE TABLE IF NOT EXISTS secrets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    encrypted_value BLOB NOT NULL,
    category TEXT DEFAULT 'general',
    created_at TEXT DEFAULT (datetime('now')),
    updated_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_secrets_category ON secrets(category);
CREATE INDEX IF NOT EXISTS idx_secrets_name ON secrets(name);

핵심: encrypted_value는 TEXT가 아니라 BLOB이다. 암호화된 데이터는 바이너리이므로 BLOB으로 저장해야 손실이 없다.

마이그레이션

앱 버전이 올라가면 스키마도 변한다. user_version 프라그마를 활용한다.

fn migrate(conn: &Connection) -> Result<()> {
    let version: i32 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;

    if version < 1 {
        conn.execute_batch(include_str!("migrations/001_init.sql"))?;
        conn.pragma_update(None, "user_version", 1)?;
    }
    if version < 2 {
        conn.execute_batch(include_str!("migrations/002_add_tags.sql"))?;
        conn.pragma_update(None, "user_version", 2)?;
    }
    Ok(())
}

앱 시작 시 migrate()를 호출하면 기존 사용자의 DB가 자동으로 최신 스키마로 업그레이드된다.

WAL 모드

읽기 성능을 높이려면 WAL(Write-Ahead Logging) 모드를 활성화한다.

conn.pragma_update(None, "journal_mode", "WAL")?;

WAL 모드에서는 읽기와 쓰기가 서로 블로킹하지 않는다. 데스크톱 앱에서 UI 스레드가 DB를 읽는 동안 백그라운드에서 쓰기가 가능해진다.

백업

SQLite의 백업은 파일 복사다. 하지만 쓰기 중에 복사하면 손상될 수 있으므로 SQLite의 Online Backup API를 사용한다.

fn backup_db(source: &Connection, dest_path: &str) -> Result<()> {
    let mut dest = Connection::open(dest_path)?;
    let backup = rusqlite::backup::Backup::new(source, &mut dest)?;
    backup.run_to_completion(5, std::time::Duration::from_millis(250), None)?;
    Ok(())
}

KeyBox에서는 설정 메뉴에서 수동 백업 버튼을 제공한다. 자동 백업은 과했다 — 사용자가 명시적으로 제어하는 게 시크릿 매니저 맥락에서 더 적절하다.

정리

SQLite는 로컬 앱의 기본 선택이다. 설치가 필요 없고, 단일 파일이고, SQL을 쓸 수 있다. 다만 암호화 데이터를 다룬다면 BLOB 타입과 마이그레이션 전략은 미리 잡아두자.