Tauri 2 완벽 가이드 — Rust 기반 초경량 크로스플랫폼 데스크탑·모바일 앱
이 글의 핵심
Tauri 2는 Rust로 쓰인 크로스플랫폼 앱 프레임워크로 Electron 대비 바이너리 크기 1/20, 메모리 사용 1/2 수준입니다. 2024년 GA된 v2부터 iOS·Android도 공식 지원해 하나의 코드베이스로 데스크탑+모바일을 모두 배포할 수 있습니다. 이 글은 프로젝트 생성·Rust↔JS IPC·권한 모델·자동 업데이트·크로스플랫폼 빌드를 실전 중심으로 정리합니다.
이 글의 핵심
Tauri는 “Electron 대신 Rust + OS 웹뷰로 더 작고 빠른 앱을 만들자”는 프로젝트로 2020년 출발, 2024년 10월 v2 정식 출시와 함께 모바일(iOS/Android) 공식 지원·확장 가능한 플러그인 시스템·크로스 플랫폼 IPC 재설계로 한 번 더 도약했습니다.
대표 사용처: 1Password 차세대 앱, Cloudflare Warp, Wealthfront 데스크탑, 여러 Rust 생태 개발 도구.
핵심 특징:
- 바이너리 크기: Electron 대비 1/20 (Win ~3MB, macOS ~8MB)
- 메모리 사용: Chromium 대신 OS 웹뷰 → RAM 1/2 수준
- Rust 백엔드: 타입 안전성·성능·크레이트 생태계
- 보안: 명시적 권한 모델(capability), CSP 강제
- 크로스플랫폼: Windows/macOS/Linux/iOS/Android 한 코드베이스
- 프런트엔드 자유: React/Vue/Svelte/Solid/Vanilla — 어떤 것이든 OK
설치
시스템 준비
- Rust:
rustup install stable - Node.js 20+: pnpm 권장
- OS별 의존성:
- Windows: Visual Studio Build Tools + WebView2 (Windows 11은 기본)
- macOS: Xcode Command Line Tools
- Linux:
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libxdo-dev libayatana-appindicator3-dev librsvg2-dev
프로젝트 생성
pnpm create tauri-app@latest
# 또는 기존 프런트 프로젝트에 Tauri 추가
pnpm add -D @tauri-apps/cli
pnpm tauri init
대화형으로 프런트 프레임워크(React/Vue/Svelte/Solid) 선택 → 프로젝트 생성 완료.
실행
pnpm tauri dev # 개발 모드 (hot reload)
pnpm tauri build # 프로덕션 빌드 + 설치 파일 생성
프로젝트 구조
my-app/
├── src/ # 프런트 (React/Vite)
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json # 앱 설정
│ ├── build.rs
│ ├── capabilities/ # 권한 정의
│ │ └── default.json
│ ├── icons/
│ └── src/
│ ├── main.rs # Rust 진입점
│ └── lib.rs
├── package.json
└── vite.config.ts
IPC: Rust ↔ JS
Rust 명령 정의
// src-tauri/src/lib.rs
use tauri::State;
use std::sync::Mutex;
struct Counter(Mutex<i32>);
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn fetch_github_issues(repo: String) -> Result<Vec<Issue>, String> {
let url = format!("https://api.github.com/repos/{}/issues", repo);
let resp = reqwest::Client::new()
.get(&url)
.header("User-Agent", "tauri-app")
.send()
.await
.map_err(|e| e.to_string())?;
resp.json::<Vec<Issue>>().await.map_err(|e| e.to_string())
}
#[tauri::command]
fn increment(counter: State<Counter>) -> i32 {
let mut n = counter.0.lock().unwrap();
*n += 1;
*n
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(Counter(Mutex::new(0)))
.invoke_handler(tauri::generate_handler![greet, fetch_github_issues, increment])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Issue {
number: u64,
title: String,
html_url: String,
}
JS에서 호출
import { invoke } from "@tauri-apps/api/core"
async function loadIssues() {
const msg = await invoke<string>("greet", { name: "JB" })
const issues = await invoke<Issue[]>("fetch_github_issues", { repo: "tauri-apps/tauri" })
return issues
}
argument 이름이 snake_case → camelCase 로 자동 변환됩니다(Builder의 invoke_handler에 .register 시 설정 가능). Rust의 Result<T, E>는 JS에서 성공/예외로 자연스럽게 매핑됩니다.
Event: 양방향 스트리밍
Rust → JS
use tauri::Emitter;
#[tauri::command]
async fn start_log_stream(app: tauri::AppHandle) {
tokio::spawn(async move {
for i in 0..100 {
app.emit("log", format!("line {}", i)).unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
});
}
import { listen } from "@tauri-apps/api/event"
const unlisten = await listen<string>("log", (event) => {
console.log(event.payload)
})
// 컴포넌트 언마운트 시 unlisten() 호출
JS → Rust
import { emit } from "@tauri-apps/api/event"
emit("user-clicked", { id: 42 })
use tauri::Listener;
app.listen("user-clicked", |event| {
println!("payload: {}", event.payload());
});
권한 (Capabilities) — v2의 핵심 변화
Tauri 2는 “프런트가 접근 가능한 API를 capability 파일에 명시적으로 허용” 하는 엄격한 모델로 바뀌었습니다.
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default app permissions",
"windows": ["main"],
"permissions": [
"core:default",
"core:event:default",
"core:window:allow-close",
"core:window:allow-minimize",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
{
"identifier": "fs:scope",
"allow": [
{"path": "$APPDATA/*"},
{"path": "$DOCUMENT/my-app/*"}
]
},
"dialog:default",
"shell:allow-open"
]
}
권한이 없는 API는 컴파일·런타임 모두에서 차단됩니다. Electron의 “기본 모두 허용” 모델과 정반대이며 공급망 보안에 훨씬 강건합니다.
파일 시스템 / 대화상자
import { readTextFile, writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs"
import { open, save } from "@tauri-apps/plugin-dialog"
const selected = await open({ multiple: false, filters: [{ name: "Markdown", extensions: ["md"] }] })
if (selected) {
const text = await readTextFile(selected as string)
// ... 편집
}
const target = await save({ defaultPath: "note.md" })
if (target) {
await writeTextFile(target, "# Hello")
}
Capability에서 허용된 경로만 접근 가능해 임의 파일 접근 공격이 불가능합니다.
스토어 / SQLite / 암호화 저장
pnpm tauri add store
pnpm tauri add sql --features sqlite
pnpm tauri add stronghold
import { load } from "@tauri-apps/plugin-store"
const store = await load("settings.json")
await store.set("theme", "dark")
await store.save()
tauri-plugin-stronghold는 패스워드 금고 수준의 암호화 저장을 제공합니다.
시스템 트레이 / 알림 / 전역 단축키
use tauri::{tray::TrayIconBuilder, menu::{Menu, MenuItem}};
let quit = MenuItem::new(app, "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&quit])?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| {
if event.id() == "quit" {
app.exit(0);
}
})
.build(app)?;
import { isPermissionGranted, requestPermission, sendNotification } from "@tauri-apps/plugin-notification"
if (!(await isPermissionGranted())) {
await requestPermission()
}
sendNotification({ title: "Tauri", body: "Build complete" })
사이드카(Sidecar) 프로세스
Rust 밖의 외부 바이너리(예: ffmpeg, python) 실행:
// tauri.conf.json
"bundle": {
"externalBin": ["binaries/ffmpeg"]
}
use tauri_plugin_shell::ShellExt;
app.shell()
.sidecar("ffmpeg")?
.args(["-i", "input.mp4", "output.mp3"])
.spawn()?;
ffmpeg 바이너리를 플랫폼별로 binaries/ffmpeg-x86_64-pc-windows-msvc.exe 등으로 준비하면 Tauri가 자동 동봉·실행합니다.
모바일 (iOS/Android)
iOS
pnpm tauri ios init
pnpm tauri ios dev # 시뮬레이터
pnpm tauri ios build # IPA 빌드 (codesign 필요)
Xcode 프로젝트가 src-tauri/gen/apple 아래 생성되고 Apple Developer 인증서로 서명합니다.
Android
pnpm tauri android init
pnpm tauri android dev
pnpm tauri android build # APK/AAB
NDK·SDK 경로 설정이 필요하며 Gradle 프로젝트가 src-tauri/gen/android에 생성됩니다.
플랫폼 분기
import { platform } from "@tauri-apps/plugin-os"
const p = await platform()
if (p === "ios" || p === "android") {
// 모바일 전용 UI
}
자동 업데이트
pnpm tauri add updater
// tauri.conf.json
"plugins": {
"updater": {
"active": true,
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"dialog": true,
"pubkey": "YOUR_UPDATER_PUBKEY"
}
}
# 빌드 시 서명
TAURI_SIGNING_PRIVATE_KEY="..." \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="..." \
pnpm tauri build
서버는 업데이트 매니페스트 JSON을 반환하고 앱이 자동으로 서명 검증 + 다운로드 + 재시작합니다. GitHub Releases + tauri-action 워크플로로 자동화하는 패턴이 일반적입니다.
배포 파이프라인: GitHub Actions
name: release
on:
push:
tags: ["v*"]
permissions: {contents: write}
jobs:
build:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-22.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: {node-version: 20}
- uses: dtolnay/rust-toolchain@stable
- uses: pnpm/action-setup@v4
with: {version: 9}
- run: pnpm install
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: ${{ github.ref_name }}
releaseName: "Release ${{ github.ref_name }}"
releaseDraft: true
prerelease: false
3개 OS 모두 병렬 빌드 + 서명 + GitHub Release 업로드까지 한 번에.
성능·번들 최적화
Cargo.toml의 release 프로파일:[profile.release] panic = "abort" codegen-units = 1 lto = true opt-level = "s" strip = true- 프런트 번들: Vite의
build.rollupOptions.treeshake: "recommended" - 아이콘: Tauri CLI가 SVG 하나로 모든 플랫폼 아이콘 생성
- macOS Universal Binary:
pnpm tauri build --target universal-apple-darwin
트러블슈팅
Linux WebKitGTK 2.40 이슈
libwebkit2gtk-4.1-dev 설치 (구버전 4.0 아님). Ubuntu 22.04+ 또는 22.10 저장소 활성.
Windows WebView2 없음
설치 프로그램이 자동 감지·다운로드하지만 오프라인 환경은 tauri.conf.json의 bundle.windows.webviewInstallMode: "embedBootstrapper"로 동봉.
코드 서명 실패 (macOS)
Apple Developer 계정·Developer ID Certificate 필수. tauri-action이 env 변수로 받음.
크기가 예상보다 큼
- 프런트 번들 분석:
vite-bundle-visualizer - 사용하지 않는 Tauri 플러그인 제거
features로 불필요한 Tauri 기능 off
IPC 성능
대용량 데이터는 IPC 대신 사이드카 + 파일/파이프로 전달. Tauri v2의 바이너리 채널(Channel) API 활용.
언제 Tauri 대신 Electron
- VS Code 같은 편집기·터미널: Monaco·xterm 등 Chromium-only 기능 의존
- Node.js 전용 생태계 필수: puppeteer, electron-store 등 그대로 재사용
- 팀이 Rust 경험 전무 + 단기 일정: Electron 학습 곡선이 낮음
신규 프로젝트 대부분은 Tauri가 미래 친화적입니다.
체크리스트
- Rust stable + 플랫폼 의존성 준비
- 명시적 권한(capability) 설계
- 자동 업데이트 서명 키 관리
- macOS/Windows 코드 서명 인증서
- 3개 OS 매트릭스 GitHub Actions
- 모바일 필요 시 iOS/Android 초기화
- CSP + Capability로 보안 하드닝
- 사용자 데이터: store/sql/stronghold 중 적합한 것
마무리
Tauri 2는 “가볍고, 안전하고, Rust스러운 데스크탑 앱”을 실제 프로덕션 수준으로 완성했습니다. 바이너리 크기와 메모리 부담이 1/10-1/20 수준이라 사용자 체감 품질이 달라지고, v2부터 모바일까지 한 코드베이스로 커버됩니다. Electron의 생태계 광활함은 여전히 매력이지만 새 프로젝트에서 사용자 경험과 보안이 중요하다면 Tauri가 기본 선택지가 되어야 합니다. 이 글의 프로젝트 구조·IPC·권한·CI 템플릿만 적용해도 첫 릴리스까지의 궤적이 훨씬 짧아질 겁니다.
관련 글
- Rust 완벽 가이드
- Electron vs Tauri 비교
- React Native 완벽 가이드
- Flutter 완벽 가이드