출시·고도화 중
Tauri 안내서 · 4/6
Tauri 앱의 상태는 두 곳에 있을 수 있습니다. 화면에만 필요한 값은 React나 Vue 같은 프런트엔드 프레임워크의 상태 관리로 충분합니다. 여러 창이 함께 보거나, 파일 · 데이터베이스 · 네트워크와 얽힌 값은 Rust 코어에 두는 편이 안전합니다. 이 장에서는 Rust 쪽에서 상태를 관리하는 방법, 프런트엔드와 데이터를 주고받는 형식, 디스크에 저장하는 플러그인을 다룹니다.
Rust 쪽 상태는 Builder의 manage로 등록하고, 명령 인자에 State<'_, T>를 적어 꺼내 씁니다. 명령은 여러 스레드에서 동시에 실행될 수 있으므로, 바꿔야 하는 값은 Mutex로 감쌉니다.
use std::sync::Mutex;
use serde::Serialize;
use tauri::State;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct Todo {
id: u32,
title: String,
is_done: bool,
}
#[derive(Default)]
struct AppState {
todos: Vec<Todo>,
next_id: u32,
}
#[tauri::command]
fn add_todo(state: State<'_, Mutex<AppState>>, title: String) -> Vec<Todo> {
let mut app = state.lock().unwrap();
let id = app.next_id;
app.next_id += 1;
app.todos.push(Todo { id, title, is_done: false });
app.todos.clone()
}
#[tauri::command]
fn list_todos(state: State<'_, Mutex<AppState>>) -> Vec<Todo> {
state.lock().unwrap().todos.clone()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(Mutex::new(AppState::default()))
.invoke_handler(tauri::generate_handler![add_todo, list_todos])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}State에 적은 타입은 manage로 등록한 타입과 정확히 같아야 합니다. Mutex<AppState>를 등록해 놓고 State<'_, AppState>로 꺼내면 실행 중에 패닉이 일어나므로 주의하세요. 명령 밖, 예를 들어 setup 훅이나 이벤트 처리기에서는 Manager 트레이트를 가져와 app.state::<Mutex<AppState>>()로 같은 값을 꺼냅니다. 잠금을 쥔 채 .await를 지나야 한다면 표준 Mutex 대신 Tokio의 비동기 Mutex를 씁니다.
명령의 인자와 반환값은 serde를 거쳐 JSON이 됩니다. 위의 #[serde(rename_all = "camelCase")] 덕분에 Rust의 is_done이 JavaScript에서는 isDone으로 보입니다. 프런트엔드에는 같은 모양의 타입을 선언해 두고 invoke의 타입 인자로 넘깁니다.
import { invoke } from "@tauri-apps/api/core";
type Todo = { id: number; title: string; isDone: boolean };
const todos = await invoke<Todo[]>("add_todo", { title: "문서 읽기" });
console.log(todos.filter((todo) => !todo.isDone).length);오류도 직렬화할 수 있어야 프런트엔드로 전달됩니다. 문자열 대신 자기 오류 타입을 쓰려면 thiserror로 정의하고 Serialize를 직접 구현하는 방식이 흔합니다. 그러면 명령 안에서 ? 연산자를 그대로 쓸 수 있습니다.
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl serde::Serialize for Error {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
#[tauri::command]
fn read_note(path: String) -> Result<String, Error> {
Ok(std::fs::read_to_string(path)?)
}설정이나 최근 연 파일 목록 같은 작은 키-값 데이터는 store 플러그인이 알맞습니다. npm run tauri add store로 추가하고, capability에 store:default가 있는지 확인합니다. 저장 파일은 앱 데이터 폴더에 만들어집니다.
import { load } from "@tauri-apps/plugin-store";
const store = await load("settings.json", { autoSave: false });
await store.set("theme", "dark");
const theme = await store.get<string>("theme");
await store.save();Rust 쪽에서는 tauri_plugin_store::StoreExt를 가져와 app.store("settings.json")?로 같은 저장소를 열 수 있으므로, 시작할 때 설정을 읽어 창을 구성하는 일도 할 수 있습니다.
파일을 직접 읽고 쓸 때는 fs 플러그인을 씁니다. 경로는 절대 경로 대신 BaseDirectory를 기준으로 적는 것이 좋습니다. 운영체제마다 다른 앱 데이터 · 설정 폴더 위치를 Tauri가 알아서 맞춰 줍니다.
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
await writeTextFile("notes.txt", "첫 메모", { baseDir: BaseDirectory.AppData });
const text = await readTextFile("notes.txt", { baseDir: BaseDirectory.AppData });fs:default는 앱 전용 폴더를 읽는 권한만 줍니다. 쓰기가 필요하면 fs:allow-appdata-write 같은 미리 정의된 권한을 더하거나, 명령별 권한에 경로 범위(scope)를 붙여 필요한 곳만 엽니다. Rust 코드에서는 app.path().app_data_dir()로 같은 폴더 경로를 얻어 표준 라이브러리로 다룰 수 있습니다.
관계형 데이터가 필요하면 sql 플러그인으로 SQLite(설정에 따라 MySQL, PostgreSQL)를 프런트엔드에서 쓰거나, Rust 쪽에서 sqlx나 rusqlite 같은 크레이트를 직접 쓰고 명령으로 결과만 내보낼 수 있습니다. 비즈니스 규칙이 많은 앱이라면 후자가 검증과 테스트를 Rust 한곳에 모을 수 있어 관리하기 쉽습니다.
manage로 등록하고 State<'_, T>로 꺼내며, 바뀌는 값은 Mutex로 감쌉니다.rename_all = "camelCase"로 양쪽 이름을 맞춥니다.BaseDirectory로 다룹니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.