3단계
IPC — command / event
25 분
IPC — command / event
프론트 (TypeScript) 와 백엔드 (Rust) 가 대화하는 두 가지 방식.
- command (invoke) — 프론트 → Rust, 결과 반환. 함수 호출처럼.
- event (emit / listen) — 양방향 · 비동기 알림. pub/sub 패턴.
1. command — Rust 측 정의
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
tokio::fs::read_to_string(&path).await.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command]가 자동으로 JSON 직렬화Result<T, String>로 실패 전파
2. command — 프론트 호출
import { invoke } from "@tauri-apps/api/core";
const msg = await invoke<string>("greet", { name: "World" });
// "Hello, World!"
try {
const content = await invoke<string>("read_file", { path: "/etc/hosts" });
} catch (e) {
console.error(e);
}
파라미터 이름이 Rust 의 snake_case 와 일치해야 자동 매핑.
3. event — 양방향
use tauri::Emitter;
#[tauri::command]
async fn start_job(app: tauri::AppHandle) -> Result<(), String> {
for i in 1..=100 {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
app.emit("job-progress", i).map_err(|e| e.to_string())?;
}
app.emit("job-done", ()).map_err(|e| e.to_string())?;
Ok(())
}
import { listen } from "@tauri-apps/api/event";
const unlisten = await listen<number>("job-progress", (e) => {
console.log("progress:", e.payload);
});
await invoke("start_job");
// 정리
unlisten();
listen 핸들러 정리를 잊으면 컴포넌트 언마운트 후에도 호출됨.
4. capabilities — 권한 화이트리스트
src-tauri/capabilities/default.json:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-open",
"fs:allow-read-text-file",
"shell:allow-open"
]
}
기본 deny. 필요한 것만 opt-in. 파일 읽기 · shell 실행 같은 민감 기능은 명시.
5. 에러 전파 패턴
#[derive(Debug, thiserror::Error, serde::Serialize)]
enum Error {
#[error("io error: {0}")]
Io(String),
#[error("not found")]
NotFound,
}
// impl From<std::io::Error> for Error { ... }
#[tauri::command]
async fn safe_op() -> Result<String, Error> { ... }
프론트에서 catch (e) 로 타입 좁은 핸들링.
6. 자주 걸리는 자리
- invoke_handler 등록 누락 —
#[tauri::command]만 써도 generate_handler! 에 추가해야 - 비동기 Rust 함수 블로킹 —
async+tokio::fs같은 비동기 I/O - 이벤트 핸들러 중복 — useEffect 마다 listen 이 쌓임. cleanup 필수
- JSON 직렬화 실패 — Rust 타입에
#[derive(serde::Serialize)]누락
하고픈 말
command 는 REST 의 GET / POST 처럼, event 는 WebSocket / SSE 처럼 생각하면 설계가 편합니다. 90% 는 command 로 충분.
Next
- 04-local-sqlite