Tauri 2 완벽 가이드 — Rust 기반 초경량 크로스플랫폼 데스크탑·모바일 앱

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 로 자동 변환됩니다(Builderinvoke_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.jsonbundle.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 완벽 가이드