codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›Tauri 2 — 데스크탑 · 모바일 한 코드베이스›4단계

4단계

로컬 SQLite

0회 조회

로컬 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

← 3단계

IPC — command / event

5단계 →

Android 빌드