4단계
로컬 SQLite
25 분
로컬 SQLite
데스크탑 · 모바일 앱에서 로컬 데이터를 저장하는 1 번 선택지. 서버도 네트워크도 필요 없고, 파일 하나.
1. 플러그인 설치
pnpm tauri add sql
자동으로 src-tauri/Cargo.toml · package.json 에 dependency 추가 + capability 갱신.
2. 마이그레이션 정의
// src-tauri/src/lib.rs
use tauri_plugin_sql::{Builder, Migration, MigrationKind};
pub fn run() {
let migrations = vec![
Migration {
version: 1,
description: "create_foods",
sql: "CREATE TABLE foods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
rating TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
kind: MigrationKind::Up,
},
];
tauri::Builder::default()
.plugin(
Builder::default()
.add_migrations("sqlite:app.db", migrations)
.build(),
)
.run(tauri::generate_context!())
.unwrap();
}
버전 번호로 멱등 · 순차 실행.
3. 프론트에서 쿼리
import Database from "@tauri-apps/plugin-sql";
const db = await Database.load("sqlite:app.db");
await db.execute(
"INSERT INTO foods (name, rating) VALUES ($1, $2)",
["김치찌개", "loved"]
);
const rows = await db.select<Food[]>(
"SELECT * FROM foods WHERE rating = $1 ORDER BY created_at DESC LIMIT $2",
["loved", 20]
);
플레이스홀더는 $1 · $2. 절대 문자열 연결로 만들지 말 것.
4. 저장 위치
- macOS —
~/Library/Application Support/<identifier>/app.db - Windows —
%APPDATA%\<identifier>\app.db - Linux —
~/.local/share/<identifier>/app.db - Android — 앱 내부 저장소
tauri.conf.json 의 identifier 에 의해 결정.
5. 저장소 계층화
// src/lib/foodDb.ts
let _db: Database | null = null;
export async function getDb() {
if (!_db) _db = await Database.load("sqlite:app.db");
return _db;
}
export async function addFood(name: string, rating: string) {
const db = await getDb();
await db.execute("INSERT INTO foods (name, rating) VALUES ($1, $2)", [name, rating]);
}
export async function listFoods(rating: string, limit = 20) {
const db = await getDb();
return db.select<Food[]>(
"SELECT * FROM foods WHERE rating = $1 ORDER BY created_at DESC LIMIT $2",
[rating, limit]
);
}
컴포넌트에서는 addFood·listFoods 만 호출. SQL 은 한 파일.
6. 백업 · 내보내기
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
const rows = await db.select<Food[]>("SELECT * FROM foods");
const path = await save({ filters: [{ name: "JSON", extensions: ["json"] }] });
if (path) await writeTextFile(path, JSON.stringify(rows, null, 2));
사용자 기기에서 데이터를 꺼낼 길은 기본 제공. 구독 · 서버 백업은 별도.
7. 자주 걸리는 자리
- 문자열 연결 SQL — SQLite도 injection 가능. 항상 파라미터
- 플레이스홀더 문법 혼동 —
?가 아니라$1·$2 - Android 에서 경로 하드코딩 — 상대 URL
sqlite:app.db유지 - 마이그레이션 순서 건너뛰기 — version 누락 없이 순차
하고픈 말
로컬 SQLite 만으로도 맛기로그 · Readingbounce 같은 앱을 실제 운영 가능. 서버가 필요 없으면 안 두는 게 장기 유지 비용에서 유리.
Next
- 05-android-build