Split View: Tauri 2 — 정말로 Electron을 대체하는가, 아니면 다른 도구인가
Tauri 2 — 정말로 Electron을 대체하는가, 아니면 다른 도구인가
프롤로그 — 왜 다시 데스크톱 프레임워크 이야기인가
"Electron은 무겁다"는 말은 너무 자주 들어서 이제 클리셰가 됐다. Slack을 띄우면 RAM 400MB가 사라지고, VS Code는 1GB까지 올라가고, 디스코드 인스톨러는 90MB다. 매번 같은 Chromium 사본이 사용자의 디스크와 메모리를 점령한다.
대안은 늘 거론됐다. 네이티브로 짜라(인력 두 배), Flutter 데스크톱(아직 어색하다), Qt(상업 라이선스), 그리고 — Tauri.
2024년 10월 Tauri 2.0이 GA됐다. 2026년 현재, 출시 후 1년 반이 지났다. 약속은 강력했다: 10MB 번들, 시스템 웹뷰, Rust 코어, iOS/Android까지. 약속만 보면 Electron의 자리를 차지할 것 같다.
그런데 실제로는? Slack은 여전히 Electron이다. VS Code도 Electron이다. Discord도 Electron이다. 큰 앱들이 마이그레이션하지 않은 이유가 있다. 동시에 SilentKeys, Voxly, Watson.ai, Pake 같은 새 앱들은 처음부터 Tauri로 짰다. 새 프로젝트와 기존 프로젝트의 선택이 갈린다.
이 글은 그 갈림을 정리한다. 아키텍처의 진짜 차이, 측정 가능한 트레이드오프(번들 크기, 메모리, 시작 시간), 시스템 웹뷰가 가져오는 그늘, 보안 모델의 작동 방식, 모바일 지원의 현실, 그리고 "우리 팀에 맞는가"를 판단하는 기준. Electron과 Tauri 둘 중 하나가 모두에게 정답이라는 식의 글은 아니다.
핵심 한 줄: Tauri는 Electron을 "더 가볍게 만든 것"이 아니라, 다른 트레이드오프를 고른 다른 도구다. 그 트레이드오프를 알면 선택이 쉬워진다.
1장 · 아키텍처 — 무엇이 어떻게 다른가
Tauri의 차별점은 한 줄로 요약할 수 있다: Chromium을 번들하지 않는다. 대신 OS의 시스템 웹뷰를 쓴다.
Electron 앱의 구성
┌────────────────────────────────────────────────┐
│ 당신의 앱 │
│ ├─ 메인 프로세스: Node.js 런타임 │
│ ├─ 렌더러 프로세스: Chromium (V8 + Blink) │
│ └─ Electron 바이너리 (~80~120MB) │
│ │
│ → 같은 OS에 같은 Chromium이 N개 사본 존재 │
└────────────────────────────────────────────────┘
Tauri 앱의 구성
┌────────────────────────────────────────────────┐
│ 당신의 앱 │
│ ├─ Rust 코어 (~3~5MB) │
│ ├─ 시스템 웹뷰 호출: │
│ │ macOS → WKWebView (Safari/WebKit) │
│ │ Windows → WebView2 (Edge/Chromium) │
│ │ Linux → WebKitGTK │
│ └─ 웹 자산(HTML/JS/CSS) │
│ │
│ → OS가 이미 갖고 있는 웹뷰를 빌려 쓴다 │
└────────────────────────────────────────────────┘
차이의 영향:
- 번들 크기: Electron 80
200MB → Tauri 240MB (대부분 5~15MB) - 메모리: Electron 200
400MB 유휴 → Tauri 3080MB 유휴 - 시작 시간: Electron 1
3초 → Tauri 0.20.8초 - 렌더링 일관성: Electron은 동일, Tauri는 OS마다 다른 엔진 (이게 핵심 트레이드오프)
Tauri의 백엔드는 Rust다. 백엔드 코드(파일 시스템, 네트워크, 시스템 API 호출)는 Rust로 짜고, 프런트엔드(React/Vue/Svelte/Solid 등)는 평소대로 웹 기술로 짠다. 둘 사이는 IPC로 메시지를 주고받는다. Electron의 main/renderer 구조와 개념적으로 같지만, "main"이 Node.js가 아니라 Rust라는 게 다르다.
한 줄 요약: Electron은 "브라우저 + Node를 통째로 들고 다닌다", Tauri는 "OS가 가진 웹뷰를 빌리고 백엔드만 Rust로 컴파일한다".
2장 · 번들 크기와 메모리 — 측정 가능한 차이
추상적 비교는 그만하고 숫자로 보자. 2026년 기준 공개 벤치마크와 마이그레이션 사례에서 모은 수치다.
번들 크기 (.dmg / .msi / .AppImage)
| 앱 유형 | Electron | Tauri | 비율 |
|---|---|---|---|
| 최소 "Hello World" | 85MB | 2.5MB | 34배 |
| 중간 규모(에디터, 채팅) | 120~160MB | 8~15MB | 약 12배 |
| 무거운 앱(IDE급) | 180~250MB | 25~40MB | 약 7배 |
| 인스톨러 평균 | 80~150MB | 3~10MB | 약 20배 |
메모리 (유휴 상태)
| 앱 | Electron | Tauri |
|---|---|---|
| 빈 윈도우 1개 | 180~250MB | 25~45MB |
| 작은 UI 화면 | 220~300MB | 35~60MB |
| 중간 복잡도 SPA | 300~500MB | 60~120MB |
시작 시간 (cold start, M2 MacBook Pro)
| 앱 | Electron | Tauri |
|---|---|---|
| 최소 앱 | 0.8~1.5초 | 0.15~0.35초 |
| 중간 규모 | 1.5~3초 | 0.4~0.9초 |
이 숫자가 의미하는 것: 번들·메모리·시작 시간은 진짜로 차이가 난다. 한 마이그레이션 사례는 "인스톨러 95% 축소, 메모리 70% 감소"를 보고했다. 측정 가능한 이득이다.
하지만 — 이 숫자가 모든 걸 결정하지는 않는다. 다음 장의 함정을 보자.
3장 · 시스템 웹뷰의 그늘 — "한 번 짜고 어디서나 돈다"의 거짓말
Tauri의 가장 큰 약점은 가장 큰 장점에서 나온다. 시스템 웹뷰라서 가볍지만, 시스템 웹뷰라서 OS마다 렌더링이 다르다.
같은 React 코드를 실행해도:
macOS → WKWebView (Safari) — Safari의 버그/지원 한계가 그대로 옴
Windows → WebView2 (Edge/Chromium) — Chromium이라 가장 호환적
Linux → WebKitGTK — 가장 뒤처짐. 일부 최신 CSS/JS API 미지원
이게 무슨 뜻인가:
backdrop-filter: macOS/Windows에서는 잘 되지만 WebKitGTK는 부분만 지원:has()셀렉터: 2026년에도 WebKitGTK 버전에 따라 갈림OffscreenCanvas: macOS/Windows OK, Linux는 케이스별- WebRTC, MediaRecorder: 플랫폼마다 코덱·동작이 다르다
- 폰트 렌더링: macOS의 antialiasing이 Windows·Linux와 다르게 보임
- 드래그 앤 드롭, 클립보드 API: 각 웹뷰의 버그가 다 보인다
Electron은 이 문제를 정의상 갖지 않는다. 어디서 돌리든 같은 Chromium이라 같은 픽셀이 나온다. 비싸지만 일관성을 산다.
Tauri로 짤 때 실제 코드는 이렇게 된다:
/* Linux WebKitGTK 폴백을 가정해야 한다 */
.frosted {
background: rgba(255, 255, 255, 0.85);
}
@supports (backdrop-filter: blur(10px)) {
.frosted {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
}
}
Linux 사용자가 적은 앱이라면 무시해도 좋다. 하지만 개발자 도구를 만든다면 Linux 사용자 비율이 30~40%일 수 있다. 그러면 모든 새 CSS/JS 기능에 폴백을 짜야 한다. "Chromium에서 되니 끝"이 안 된다.
한 줄 요약: Tauri는 "한 번 짜고 어디서나 돈다"가 아니라 "한 번 짜고 OS 3개에서 테스트한다"다. Electron보다 QA 비용이 더 든다.
4장 · 보안 모델 — allowlist에서 capabilities로
Tauri 2의 큰 변화 중 하나가 보안 모델이다. v1의 allowlist는 단순한 켜기/끄기 토글이었다 — "fs API를 켤지 끌지". 이게 한계에 부딪혀서 v2는 permissions / scopes / capabilities 3계층으로 재설계됐다.
개념 3개
- Permissions: Tauri 명령에 대한 켜기/끄기 (예:
fs:allow-read-file) - Scopes: 명령의 파라미터 검증 (예: "이 경로 패턴만 읽기 허용")
- Capabilities: 권한과 스코프를 윈도우/웹뷰에 묶어 부여
이걸 JSON/TOML로 선언한다. src-tauri/capabilities/ 디렉터리에 파일을 두면 자동으로 활성화된다.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "메인 윈도우에 부여하는 권한 묶음",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APPDATA/notes/*" },
{ "path": "$DOCUMENT/MyApp/**" }
],
"deny": [
{ "path": "$HOME/.ssh/*" }
]
},
"shell:allow-open"
]
}
이 설정이 의미하는 것:
- 메인 윈도우만 이 권한들을 받는다 (다른 윈도우는 별도 capability 필요)
- 파일 시스템 읽기/쓰기는 켜져 있지만 —
$APPDATA/notes/와$DOCUMENT/MyApp/하위만 $HOME/.ssh/*는 명시적 거부shell:open은 허용 (이메일/URL 열기용)
Electron에는 이런 선언적 모델이 없다. Electron에서는 보안이 코드 패턴이다 — contextIsolation: true, nodeIntegration: false, preload 스크립트, IPC 화이트리스트를 손으로 짜야 한다. 잊으면 RCE 취약점이 된다.
Tauri의 모델이 더 안전한 이유:
- 선언적: 권한이 코드가 아니라 설정이라 감사하기 쉽다
- 세분화: 파일 시스템 전체가 아니라 특정 경로만
- 기본 거부: capability를 명시하지 않으면 IPC 명령이 차단된다
- 윈도우별: 메인 윈도우와 about 윈도우의 권한을 분리
물론 Electron도 신경 써서 짜면 같은 수준이 된다. 차이는 — Tauri는 그게 기본값이고, Electron은 그게 추가 작업이다.
한 줄 요약: capabilities는 "기본 안전한 IPC"를 강제한다. Electron에서 신경 안 쓰면 RCE가 되는 패턴이, Tauri에서는 capability 파일을 안 적으면 IPC가 그냥 거부된다.
5장 · IPC 계약 — 명령 하나를 끝까지 보기
추상적 이야기 그만하고, 실제 IPC 명령 하나를 처음부터 끝까지 보자. 시나리오: 프런트엔드에서 "노트 파일 저장" 버튼을 누르면 백엔드(Rust)가 디스크에 쓴다.
Rust 측: 명령 정의
src-tauri/src/commands.rs:
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::AppHandle;
use tauri_plugin_fs::FsExt;
#[derive(Debug, Serialize, Deserialize)]
pub struct Note {
pub id: String,
pub title: String,
pub body: String,
}
#[derive(Debug, Serialize)]
pub struct SaveResult {
pub bytes_written: usize,
pub path: String,
}
#[tauri::command]
pub async fn save_note(
app: AppHandle,
note: Note,
) -> Result<SaveResult, String> {
// 1) 입력 검증 — IPC 경계는 신뢰할 수 없다
if note.id.is_empty() || note.id.contains('/') || note.id.contains('\\') {
return Err("invalid note id".into());
}
if note.body.len() > 10 * 1024 * 1024 {
return Err("note too large (>10MB)".into());
}
// 2) capability가 허용하는 경로 안에서만 작업
let app_data = app
.path()
.app_data_dir()
.map_err(|e| format!("no app data dir: {e}"))?;
let mut path: PathBuf = app_data.join("notes");
std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
path.push(format!("{}.json", note.id));
// 3) 직렬화하고 쓴다
let body = serde_json::to_vec_pretty(¬e).map_err(|e| e.to_string())?;
let bytes = body.len();
std::fs::write(&path, body).map_err(|e| e.to_string())?;
Ok(SaveResult {
bytes_written: bytes,
path: path.display().to_string(),
})
}
src-tauri/src/lib.rs:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![
commands::save_note,
])
.run(tauri::generate_context!())
.expect("tauri 실행 실패");
}
TypeScript 측: 호출
src/lib/notes.ts:
import { invoke } from '@tauri-apps/api/core'
export interface Note {
id: string
title: string
body: string
}
export interface SaveResult {
bytesWritten: number
path: string
}
export async function saveNote(note: Note): Promise<SaveResult> {
// invoke는 Rust 측 #[tauri::command] 이름과 1:1로 매핑된다
// 직렬화는 자동 (serde ↔ JSON)
// 단, 필드 이름은 snake_case ↔ camelCase 매핑 규칙 확인 필요
const raw = await invoke<{ bytes_written: number; path: string }>('save_note', {
note,
})
return { bytesWritten: raw.bytes_written, path: raw.path }
}
React 컴포넌트에서:
import { useState } from 'react'
import { saveNote } from './lib/notes'
export function NoteEditor() {
const [title, setTitle] = useState('')
const [body, setBody] = useState('')
const [status, setStatus] = useState<string | null>(null)
async function onSave() {
try {
const result = await saveNote({
id: crypto.randomUUID(),
title,
body,
})
setStatus(`저장됨: ${result.path} (${result.bytesWritten} bytes)`)
} catch (e) {
setStatus(`실패: ${String(e)}`)
}
}
return (
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea value={body} onChange={(e) => setBody(e.target.value)} />
<button onClick={onSave}>저장</button>
{status && <p>{status}</p>}
</div>
)
}
이게 어떻게 안전한가
- capability에서 명시적 허용 —
src-tauri/capabilities/main.json에save_note가 있는 윈도우만 호출 가능 - Rust 입력 검증 — id에 슬래시가 있으면 디렉터리 탈출 시도, 크기 제한도 명시
- 경로 한정 —
app_data_dir()로만 작업, 사용자가 임의 경로를 주입할 수 없음 - 타입 안전 — Rust 측은 컴파일러가, TS 측은 invoke의 제네릭이 검증
Electron으로 같은 걸 짜면 preload + contextBridge + IPC 핸들러 + 명시적 화이트리스트를 직접 구성해야 한다. 분량은 비슷한데, 잊으면 보안 구멍이 된다.
한 줄 요약: Tauri의 IPC는 Rust의 타입 시스템과 capability 시스템이 함께 강제한다. 코드 옆에 보안이 붙어 있다.
6장 · 모바일 — Tauri 2의 진짜 큰 변화
Tauri 1은 데스크톱만 지원했다. v2가 발표하면서 가장 크게 광고된 게 iOS · Android 지원이다. 2026년 현재, 이게 진짜로 어디까지 왔는지가 중요하다.
무엇이 되는가
- iOS와 Android 빌드가 안정 (Tauri 2.0 GA부터)
- 같은 웹 프런트엔드 코드를 데스크톱과 공유
- Rust 백엔드를 모바일에서도 호출 (네이티브 플러그인은 Swift/Kotlin/Java로 작성 가능)
- 기본 플러그인(파일 시스템, 노티피케이션, 다이얼로그 등) 중 상당수가 모바일에서 작동
무엇이 아직 거친가
- 데스크톱 플러그인 = 모바일 플러그인 등호 아님. 일부 플러그인은 데스크톱만, 일부는 모바일만 지원
- Tauri 팀 스스로 인정 — "모바일 일등 시민" 약속은 과도했고, 커뮤니티와 함께 만들어가는 중이라고 GA 회고에 적혀 있다
- iOS 사이드로딩의 어려움 (Apple 정책), Android는 상대적으로 자유
- Flutter나 React Native만큼 모바일에 최적화된 UX를 자동으로 주지는 않는다 — 어차피 웹뷰 기반이라 모바일 네이티브 느낌을 위한 추가 작업 필요
언제 Tauri 모바일을 선택하나
| 시나리오 | 추천 |
|---|---|
| 이미 Tauri 데스크톱 앱이 있고, 같은 코드를 모바일로 확장 | Tauri 모바일 좋음 |
| 모바일 우선 앱, 데스크톱은 부차적 | React Native / Flutter / 네이티브 |
| 데스크톱과 모바일 모두 일급, 코드 공유 중요 | 진지하게 Tauri 검토 |
| 모바일 네이티브 UX가 핵심(제스처, 애니메이션) | 네이티브 또는 Flutter |
한 줄 요약: Tauri 모바일은 "데스크톱 앱의 모바일 동반자"로는 훌륭하고, "모바일 우선 앱의 기반"으로는 아직 React Native/Flutter만큼 무르익지 않았다.
7장 · Electron · Wails · Neutralino · 네이티브 — 비교 매트릭스
각 도구의 자리를 정리한다.
| 항목 | Tauri 2 | Electron | Wails | Neutralino | 네이티브 |
|---|---|---|---|---|---|
| 백엔드 언어 | Rust | Node.js | Go | C/C++(코어) | Swift/Kotlin/C++/C# |
| 렌더링 | 시스템 웹뷰 | Chromium 번들 | 시스템 웹뷰 | 시스템 웹뷰 | 네이티브 UI |
| 번들 크기 | 5~40MB | 80~200MB | 10~25MB | 2~10MB | 1~20MB |
| 메모리 유휴 | 30~80MB | 200~400MB | 50~100MB | 30~70MB | 20~80MB |
| 모바일 | iOS/Android | 없음 | 없음 | 없음 | 우선 시민 |
| 렌더링 일관성 | 낮음(OS별 다름) | 높음 | 낮음 | 낮음 | 높음 |
| 학습 곡선 | 높음(Rust) | 낮음(JS) | 중간(Go) | 낮음 | 가장 높음 |
| 보안 모델 | capabilities(선언적) | 코드 패턴 | 메소드 바인딩 | allowList | OS API |
| 성숙도 | 높음(2024 GA) | 매우 높음 | 중간 | 중간 | 최고 |
| 생태계 | 빠르게 성장 | 거대 | 작음 | 작음 | OS 의존 |
| 기업 후원 | 재단(독립) | OpenJS | 커뮤니티 | 커뮤니티 | 플랫폼 벤더 |
각 도구의 한 줄 요약
- Tauri 2: 가볍고 안전한 데스크톱·모바일 앱. Rust를 배울 의지가 있다면 베스트.
- Electron: 가장 폭넓은 생태계, 일관된 렌더링. 무거움을 받아들이는 대가로 빠른 개발.
- Wails: Go 팀의 자연스러운 선택. Go의 단순함을 그대로 데스크톱에 가져옴. 다만 일부 기능(멀티 윈도우 등)이 아직 진행 중.
- Neutralino: 초경량. 컴파일 없이 Node 비슷한 API. 가장 가볍지만 가장 미성숙.
- 네이티브: 플랫폼당 한 팀씩. 최고의 UX, 가장 비싸다.
8장 · Rust 학습 곡선 — 프런트엔드 팀의 진짜 비용
Tauri의 가장 큰 진입 장벽은 번들 크기도, 시스템 웹뷰도 아니다. Rust다. 프런트엔드 중심 팀에 Rust를 도입하는 비용을 정직하게 따져보자.
좋은 소식
- Tauri는 "프런트엔드는 평소대로 + 백엔드만 Rust" 구조다. 백엔드 코드가 적은 단순 앱이라면 Rust 코드는 IPC 명령 몇 개와 main.rs 정도로 끝난다
- Tauri CLI가 보일러플레이트의 90%를 만들어준다 — Rust 빌드 시스템(cargo), 크로스 컴파일, 코드 사이닝 설정을 대부분 추상화
- ChatGPT/Claude 같은 AI 도구가 Rust 코드 생성을 잘한다. 단순 IPC 명령은 AI가 대부분 짜준다
- 백엔드가 단순하면 Rust의 어려운 부분(라이프타임, 비동기 깊이, unsafe)을 거의 만나지 않음
나쁜 소식
- 백엔드가 복잡해지는 순간 — DB 연결 풀, 비동기 작업, 동시성 처리 — Rust의 학습 곡선이 가팔라진다
- 컴파일 시간이 길다. 초기 빌드 5
15분, 증분 빌드 530초. Electron은 즉시 리로드 - 에러 메시지가 친절하지만 양이 많다. JS 한 줄 에러가 Rust에서는 30줄짜리 트레잇 바운드 설명이 될 수 있음
- 팀 내 Rust 코드를 리뷰할 사람이 1명뿐이면 그 사람이 병목이 된다
- 의존성(crate) 보안 감사. JS의 npm audit보다 도구가 적고, 일부 crate는 unsafe를 많이 씀
현실적인 결정 기준
| 팀 상황 | 추천 |
|---|---|
| 프런트엔드 전담, Rust 경험 없음, 백엔드 단순 | Tauri 시도해볼 만함 |
| 프런트엔드 전담, 백엔드 복잡(DB·동시성) | Electron + Node 백엔드 또는 사이드카 패턴 |
| Rust 경험 있는 팀원 1명 이상 | Tauri 강력 추천 |
| 시스템 프로그래밍 배경 있는 팀 | Tauri 자연스러움 |
| 6개월 안에 출시해야 함, 일정 빡빡 | Electron — 모든 게 검증돼 있음 |
흥미로운 패턴: 많은 팀이 사이드카 패턴으로 절충한다. Tauri 메인은 가볍게 두고, 무거운 백엔드 로직(예: Python 머신러닝, Node 서비스)은 별도 프로세스로 띄워 Tauri가 관리. 이러면 Rust 학습 비용을 최소화하면서 Tauri의 번들·메모리 이득은 챙긴다.
한 줄 요약: 프런트엔드 팀에게 Rust는 진짜 비용이다. 백엔드 복잡도가 낮으면 감당할 만하고, 높으면 다른 도구가 낫다.
9장 · 실제로 Tauri 2로 만들어진 앱들
2026년 현재 Tauri 2로 출시된 주목할 만한 앱들. 이름과 카테고리, 그리고 왜 Tauri를 골랐는지 추정.
| 앱 | 카테고리 | 왜 Tauri인가(추정) |
|---|---|---|
| SilentKeys | 프라이버시 우선 음성 받아쓰기 | 온디바이스 ML, 작은 번들, 시스템 통합 |
| Voxly | AI 음성 받아쓰기 | 빠른 시작, 작은 메모리, 시스템 트레이 통합 |
| Watson.ai | 회의 녹음 · 추출 | 백그라운드 동작, 시스템 권한 정교한 제어 |
| Pake | 웹페이지를 데스크톱 앱으로 | 작은 번들이 핵심 가치(Electron보다 명확한 이득) |
| Rivet | AI 에이전트 비주얼 프로그래밍 | 무거운 IDE 같은 UX, 메모리 절약 중요 |
| XGetter | 동영상 · 오디오 다운로더 | 시스템 통합, 작은 인스톨러 |
| Kunobi / Kubeli | Kubernetes 데스크톱 클라이언트 | 개발자 도구, 빠른 시작, MCP 서버 내장 |
공통 패턴이 보인다:
- 새 프로젝트 — 기존 Electron 앱을 옮긴 게 아니라 처음부터 Tauri 선택
- 작은~중간 규모 — 거대 IDE가 아니라 단일 목적이 명확한 앱
- 시스템 통합 중요 — 트레이, 백그라운드, 글로벌 단축키, 시스템 권한
- 메모리 민감 — 백그라운드에서 계속 도는 앱(받아쓰기, 회의 녹음 등)
- 개발자 또는 파워 유저 타깃 — 사용자가 Linux 비율도 일정 부분
반대로 마이그레이션 사례가 적은 이유:
- 기존 Electron 앱의 코드 자산이 크다 — Rust로 백엔드 다시 짜는 비용
- 사용자가 이미 만족 — 깨지지 않은 걸 고치지 마라
- Linux 사용자 비율이 낮으면 시스템 웹뷰 차이를 신경 안 써도 됨
한 줄 요약: Tauri는 "새 프로젝트에서 의식적으로 선택"되는 경우가 압도적이다. 기존 Electron 앱 마이그레이션은 큰 동기가 있을 때만 정당화된다.
10장 · 언제 무엇을 고르나 — 의사결정 트리
시작:
│
├─ 모바일이 우선인가?
│ ├─ 예 → React Native / Flutter / 네이티브
│ └─ 아니오 → 다음
│
├─ Linux를 진지하게 지원해야 하나?
│ ├─ 예, Linux 사용자가 30%+ → 시스템 웹뷰 차이를 감수할 수 있나?
│ │ ├─ 예 → Tauri / Wails
│ │ └─ 아니오, 픽셀 일관성 필요 → Electron
│ └─ 아니오 또는 적음 → 다음
│
├─ 팀에 Rust 경험이 있나?
│ ├─ 예 또는 학습 의지 있음 → Tauri 강력 추천
│ ├─ Go 경험 있음 → Wails 검토
│ └─ JS만 가능, 학습 시간 없음 → Electron
│
├─ 번들 크기 · 메모리가 진짜로 중요한가?
│ ├─ 예(백그라운드 앱, 저사양 기기 타깃) → Tauri
│ └─ 아니오, 사용자가 신경 안 씀 → Electron이 빠름
│
├─ 백엔드 로직이 복잡한가?
│ ├─ 예, DB · 동시성 많음 → Electron 또는 Tauri + 사이드카
│ └─ 아니오, 주로 UI → Tauri 또는 Neutralino
│
└─ 일정이 빡빡한가? (3~6개월)
├─ 예 → Electron (모든 게 검증됨)
└─ 아니오, 1년+ → Tauri 학습 비용 회수 가능
단순 규칙
- 새 프로젝트, 단순한 백엔드, 번들 크기 중요 → Tauri
- 기존 Electron 앱, 사용자 만족 → 그대로 유지
- AAA급 데스크톱 UX, Linux 일급 시민 → Electron
- 데스크톱 + 모바일 코드 공유 중요 → Tauri 또는 Flutter
- Go 팀 → Wails
- 번들 정말 1MB라도 줄이고 싶음 → Neutralino (대신 미성숙)
- OS별 최고의 UX, 인력 무한 → 네이티브
11장 · 안티패턴과 함정
Tauri를 선택해놓고 후회하는 패턴들.
"Electron처럼 짜면 되겠지"
- 증상: Rust 백엔드를 거대한 단일 파일로 만들고 모든 IPC 명령을 거기 박는다
- 결과: 컴파일 시간 폭발, 변경 비용 증가, 테스트 불가
- 대신: Rust 모듈로 도메인 분리(
commands/,services/,models/), 명령은 얇게(검증 + 서비스 호출)
"capability를 다 켜기"
- 증상:
core:default에 더해 모든 fs/shell/dialog 권한을 와이드카드로 켜기 - 결과: 보안 모델의 핵심 가치를 버림. Electron의 위험을 그대로 얻음
- 대신: 진짜 필요한 것만, 스코프를 좁게. 윈도우별 capability 분리
"Rust 비동기를 처음부터 깊게"
- 증상: 단순 IPC를
tokio::spawn과 채널 6개로 구현 - 결과: 학습 곡선 폭발, 데드락, 컴파일 에러의 늪
- 대신: 처음에는
async fn+ Tauri의 기본 런타임. 비동기가 진짜 필요할 때만 깊이 들어감
"시스템 웹뷰 차이를 무시"
- 증상: macOS에서만 개발, 출시 직전 Linux 테스트
- 결과: WebKitGTK에서 절반의 UI가 깨짐
- 대신: 첫 주부터 OS 3개 CI에서 빌드, 핵심 화면은 모든 OS에서 스크린샷 비교
"프런트엔드도 Rust로 짜겠다"
- 증상: Leptos/Yew/Dioxus로 프런트까지 Rust
- 결과: Rust 학습 곡선 2배, 생태계는 React/Vue의 100분의 1
- 대신: 프런트는 평소 도구(React/Vue/Svelte), 백엔드만 Rust. Tauri의 설계가 그렇게 돼 있다
"AI 에이전트에게 Tauri 앱을 통째로 짜라고 요청"
- 증상: 에이전트가 capability 설정과 Rust IPC 패턴을 어설프게 만들어 보안 구멍
- 결과: 권한이 너무 넓거나, 입력 검증이 빠지거나, 사이드 채널이 열림
- 대신: 에이전트에게 capability와 IPC 명령은 사람이 리뷰, 단위 테스트는 IPC 경계 케이스를 명시적으로 강제
12장 · 마이그레이션 — Electron에서 Tauri로 옮긴다면
옮기는 게 정당화되는 시나리오가 있다면, 단계는 다음과 같다.
1. 측정: 현재 Electron 앱의 번들, 메모리, 시작 시간을 기록
2. 백엔드 인벤토리: Node 의존성을 모두 나열 — 어떤 게 Rust crate로 대체되나?
3. 프런트 분리: UI는 그대로 가져갈 수 있는지(대부분 가능)
4. IPC 매핑: 현재 ipcMain 핸들러 하나하나를 Tauri 명령으로 매핑
5. capability 설계: 각 IPC가 무엇에 접근하는지 정리 → capabilities 파일
6. 시스템 통합: 트레이/노티/단축키를 Tauri 플러그인으로 재구현
7. 크로스 OS 테스트: 첫날부터 3개 OS에서 빌드, 시각 회귀 테스트
8. 측정 재확인: 약속된 수치(메모리/번들)가 실제로 나오는지 검증
9. 베타 사용자 → 점진적 출시
옮기지 말아야 할 신호
- Node 의존성에 Rust 대체가 없는 게 많음(특정 머신러닝 라이브러리 등)
- 사용자 70%가 macOS, 시스템 웹뷰 차이가 어차피 문제 안 됨, 메모리도 만족
- 팀에 Rust 가능한 사람이 0명, 학습할 6개월이 없음
- 앱이 사용자에게 충분히 빠르고, 번들 크기 불만이 없음
옮기는 게 정당화되는 신호
- 백그라운드에서 24/7 도는 앱이라 메모리가 사용자 컴퓨터에 실제 부담
- 인스톨러 크기가 다운로드 전환율에 영향(저사양 / 저대역 시장)
- 보안 감사를 통과해야 하고 Electron의 보안 패턴을 다 정리하는 게 더 비쌈
- 모바일 확장 계획이 있고, 데스크톱 코드를 재사용하고 싶음
에필로그 — 체크리스트, 안티패턴, 다음 글 예고
Tauri 2는 "Electron보다 가벼운 무언가"가 아니라 다른 트레이드오프를 고른 다른 도구다. 시스템 웹뷰의 그늘, Rust 학습 곡선, 작은 생태계를 받아들이고 — 번들·메모리·보안 모델의 이득을 챙긴다. 새 프로젝트, 단순한 백엔드, 번들이 중요한 상황이라면 합리적 선택이다. 기존 Electron 앱은 강한 동기가 없으면 옮기지 않아도 된다.
Tauri 선택 체크리스트
- 새 프로젝트인가, 기존 마이그레이션인가? — 새 프로젝트라면 진입 비용이 정당화됨
- 백엔드 복잡도는? — 단순할수록 Tauri가 자연스러움
- 모바일 계획이 있는가? — 있다면 Tauri 2의 모바일 지원이 매력적
- 팀의 Rust 경험은? — 0명이면 학습 일정을 일정에 더해야 함
- Linux 사용자 비율은? — 높으면 시스템 웹뷰 QA 비용을 계획에 포함
- 번들·메모리가 진짜 중요한가? — 백그라운드 앱, 저사양 기기 타깃이면 이득
- 일정 여유는? — 빡빡하면 Electron, 1년+면 Tauri 학습 회수 가능
- capability 설계를 일찍 시작했는가? — 보안 모델이 코드 구조를 좌우
- 사이드카 패턴이 필요한가? — 무거운 백엔드는 별도 프로세스로 분리
- OS 3개 CI를 첫 주에 구성했는가? — 마지막 주에 발견하면 늦음
안티패턴
| 안티패턴 | 왜 나쁜가 | 대신 |
|---|---|---|
| 번들 크기만 보고 Tauri 선택 | Rust 학습 비용과 QA 비용 무시 | 팀과 일정도 같이 평가 |
| Electron 코드를 그대로 Rust로 1:1 포팅 | Rust 관용구가 달라 어색 | 도메인부터 다시 설계 |
| capability를 와이드카드로 켜기 | 보안 모델의 가치 소멸 | 진짜 필요한 권한만, 스코프 좁게 |
| Linux 테스트를 마지막에 | 출시 직전 발견 | 첫 주부터 3개 OS CI |
| 프런트도 Rust로 짜기(Leptos 등) | 생태계 100분의 1 | 프런트는 React/Vue, 백엔드만 Rust |
| 거대한 commands.rs 하나 | 컴파일 시간 폭발 | 도메인별 모듈로 분리 |
| Rust 비동기를 깊게 처음부터 | 학습 곡선 절벽 | 동기로 시작, 필요할 때만 비동기 |
| 시스템 웹뷰의 일관성 가정 | macOS만 보고 출시 | CSS·JS 기능은 폴백 + 모든 OS 시각 회귀 |
| 모바일을 React Native 대체로 기대 | 모바일 우선 UX 아직 부족 | 데스크톱 동반자로 사용 |
| 마이그레이션을 ROI 없이 강행 | 코드 자산 낭비, 사용자 영향 | 측정 가능한 이득이 있을 때만 |
다음 글 예고
다음 글은 **"Rust 비동기의 함정 — async/await, tokio, send 트레이트, 그리고 라이프타임이 만나는 곳"**이다. 이번 글이 "Tauri를 선택할까"의 글이라면, 다음 글은 "Tauri 백엔드가 복잡해지기 시작할 때 무엇을 만나는가"의 글이다. Rust 비동기는 강력하지만 함정이 많다 — Send/Sync 트레이트, 라이프타임과 비동기의 상호작용, 채널 선택(mpsc / oneshot / broadcast), 셀렉트 패턴, 그리고 비동기 컨텍스트에서 동기 코드를 실행하는 법까지. Tauri 백엔드든, 백엔드 마이크로서비스든, Rust 비동기를 진지하게 쓰는 모든 곳에 적용된다.
참고 / References
- Tauri 2.0 Stable Release 공지
- Tauri 공식 사이트
- Tauri 아키텍처 문서
- Tauri 보안 모델
- Tauri Capabilities 레퍼런스
- Tauri Permissions 가이드
- Tauri 모바일 알파 발표
- Tauri GitHub 저장소
- Awesome Tauri — 앱 · 플러그인 모음
- Made with Tauri — 사례 쇼케이스
- Electron 공식 사이트
- Wails 공식 사이트
- Wails vs Tauri 비교 토론
- Neutralino.js 공식
- Tauri 1.3 — allowlist 초기 설명
- Roadmap to Tauri 2.0
- Tauri vs Electron — 성능과 트레이드오프(Hopp)
- Tauri 격리 패턴
- Evil Martians — Tauri 사이드카 패턴
Tauri 2 — Does It Actually Replace Electron, or Is It a Different Tool?
Prologue — Why we are talking about desktop frameworks again
"Electron is heavy" has been said so often it became a cliché. Boot Slack and 400MB of RAM is gone. VS Code climbs to 1GB. The Discord installer is 90MB. Every time, the same copy of Chromium occupies the user's disk and memory.
Alternatives have been proposed forever. Write native (double the headcount), Flutter desktop (still awkward), Qt (commercial license), and — Tauri.
Tauri 2.0 went GA in October 2024. In 2026, we are eighteen months past that release. The promises were strong: 10MB bundles, system webview, Rust core, iOS and Android support. On paper, it looked like Electron's replacement.
In practice? Slack is still Electron. VS Code is still Electron. Discord is still Electron. The big apps have reasons not to migrate. At the same time, fresh apps like SilentKeys, Voxly, Watson.ai, and Pake chose Tauri from day one. New projects and existing projects diverge.
This post walks the divide. The real architectural differences, measurable trade-offs (bundle size, memory, startup time), the shadow the system webview casts, how the security model works, the actual state of mobile support, and how to decide whether Tauri fits your team. Neither Electron nor Tauri is the right answer for everyone.
One-line take: Tauri is not "Electron made lighter." It is a different tool with different trade-offs. Know the trade-offs and the decision becomes easy.
1. Architecture — what is actually different
The core difference fits in one sentence: Tauri does not bundle Chromium. It uses the OS's system webview instead.
Anatomy of an Electron app
┌────────────────────────────────────────────────┐
│ Your app │
│ ├─ Main process: Node.js runtime │
│ ├─ Renderer process: Chromium (V8 + Blink) │
│ └─ Electron binary (~80-120MB) │
│ │
│ → N copies of the same Chromium on the OS │
└────────────────────────────────────────────────┘
Anatomy of a Tauri app
┌────────────────────────────────────────────────┐
│ Your app │
│ ├─ Rust core (~3-5MB) │
│ ├─ System webview call: │
│ │ macOS → WKWebView (Safari/WebKit) │
│ │ Windows → WebView2 (Edge/Chromium) │
│ │ Linux → WebKitGTK │
│ └─ Web assets (HTML/JS/CSS) │
│ │
│ → Borrows the webview the OS already ships │
└────────────────────────────────────────────────┘
Consequences of the difference:
- Bundle size: Electron 80-200MB, Tauri 2-40MB (typically 5-15MB)
- Memory: Electron 200-400MB at idle, Tauri 30-80MB at idle
- Startup time: Electron 1-3 seconds, Tauri 0.2-0.8 seconds
- Rendering consistency: Electron is identical everywhere; Tauri runs a different engine on each OS (this is the central trade-off)
Tauri's backend language is Rust. Backend code — filesystem, network, system APIs — is written in Rust. The frontend (React, Vue, Svelte, Solid, whatever) stays the same. The two talk over IPC. Conceptually identical to Electron's main/renderer split, except "main" is Rust instead of Node.
Summary: Electron ships a whole browser plus Node. Tauri borrows the OS's webview and compiles only the backend to Rust.
2. Bundle size and memory — the measurable difference
Enough abstraction. Numbers, from 2026 public benchmarks and migration reports.
Bundle size (.dmg / .msi / .AppImage)
| App type | Electron | Tauri | Ratio |
|---|---|---|---|
| Minimal "Hello World" | 85MB | 2.5MB | 34x |
| Medium (editor, chat) | 120-160MB | 8-15MB | ~12x |
| Heavy (IDE-class) | 180-250MB | 25-40MB | ~7x |
| Installer average | 80-150MB | 3-10MB | ~20x |
Memory (idle)
| App | Electron | Tauri |
|---|---|---|
| Single empty window | 180-250MB | 25-45MB |
| Small UI surface | 220-300MB | 35-60MB |
| Medium-complexity SPA | 300-500MB | 60-120MB |
Cold start time (M2 MacBook Pro)
| App | Electron | Tauri |
|---|---|---|
| Minimal | 0.8-1.5s | 0.15-0.35s |
| Medium | 1.5-3s | 0.4-0.9s |
What these numbers mean: bundle, memory, and startup really are different. One migration report cited 95% installer-size reduction and 70% lower memory. The wins are measurable.
But — the numbers don't decide everything. Read the next chapter.
3. The system-webview shadow — "write once, run anywhere" is a lie
Tauri's biggest weakness comes from its biggest strength. System webview makes it light; system webview also means rendering differs by OS.
The same React code, executed on:
macOS → WKWebView (Safari) — inherits Safari bugs and gaps
Windows → WebView2 (Edge/Chromium) — most compatible since it is Chromium
Linux → WebKitGTK — most behind; some modern CSS/JS APIs missing
What this means in practice:
backdrop-filter: fine on macOS/Windows, partial on WebKitGTK:has()selector: in 2026 still varies by WebKitGTK versionOffscreenCanvas: macOS/Windows ok, Linux case-by-case- WebRTC, MediaRecorder: codecs and behavior differ per platform
- Font rendering: macOS antialiasing looks different from Windows/Linux
- Drag-and-drop, Clipboard API: each webview's quirks show through
Electron does not have this problem by definition. Everywhere you run it, the same Chromium produces the same pixels. Expensive, but you buy consistency.
Real Tauri code learns to look like this:
/* Assume a WebKitGTK fallback on Linux */
.frosted {
background: rgba(255, 255, 255, 0.85);
}
@supports (backdrop-filter: blur(10px)) {
.frosted {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
}
}
If your Linux share is tiny, ignore it. But if you build developer tooling, Linux can be 30-40% of users. Then every new CSS/JS feature needs a fallback. "Works in Chromium, ship it" is no longer enough.
Summary: Tauri is not "write once, run anywhere." It is "write once, test on three OSes." The QA bill is higher than Electron's.
4. The security model — from allowlist to capabilities
A major change in Tauri 2 is the security model. The v1 allowlist was a flat on/off toggle: "enable the fs API or not." It hit its limits, so v2 was redesigned around three concepts: permissions, scopes, capabilities.
Three concepts
- Permissions: on/off toggles for Tauri commands (e.g.,
fs:allow-read-file) - Scopes: parameter validation for commands (e.g., "only these path patterns")
- Capabilities: bundles of permissions and scopes attached to specific windows/webviews
You declare them in JSON or TOML. Files dropped into src-tauri/capabilities/ are enabled automatically.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Permissions granted to the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APPDATA/notes/*" },
{ "path": "$DOCUMENT/MyApp/**" }
],
"deny": [
{ "path": "$HOME/.ssh/*" }
]
},
"shell:allow-open"
]
}
What this configuration means:
- Only the main window receives these permissions (other windows need their own capability)
- Filesystem read/write is enabled — but only under
$APPDATA/notes/and$DOCUMENT/MyApp/ $HOME/.ssh/*is explicitly deniedshell:openis allowed (for opening email and URLs)
Electron has no equivalent declarative model. Security in Electron is a code pattern — contextIsolation: true, nodeIntegration: false, preload scripts, a hand-maintained IPC whitelist. Forget any one of them and you ship an RCE.
Why the Tauri model is safer:
- Declarative: permissions are config, not code — easy to audit
- Granular: not "filesystem on or off" but specific paths
- Default deny: a command without a capability gets a hard "no"
- Per-window: a main window and an about window can have separate permissions
Electron can match this if you're careful. The difference is — in Tauri it is the default, in Electron it is extra work.
Summary: capabilities force "safe IPC by default." A pattern that becomes RCE in Electron if forgotten just gets rejected in Tauri if no capability is declared.
5. The IPC contract — one command, end to end
Enough abstraction. Walk a real IPC command end to end. Scenario: clicking "Save Note" in the frontend triggers a Rust backend that writes to disk.
Rust side: command definition
src-tauri/src/commands.rs:
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::AppHandle;
use tauri_plugin_fs::FsExt;
#[derive(Debug, Serialize, Deserialize)]
pub struct Note {
pub id: String,
pub title: String,
pub body: String,
}
#[derive(Debug, Serialize)]
pub struct SaveResult {
pub bytes_written: usize,
pub path: String,
}
#[tauri::command]
pub async fn save_note(
app: AppHandle,
note: Note,
) -> Result<SaveResult, String> {
// 1) Validate input — the IPC boundary is untrusted
if note.id.is_empty() || note.id.contains('/') || note.id.contains('\\') {
return Err("invalid note id".into());
}
if note.body.len() > 10 * 1024 * 1024 {
return Err("note too large (>10MB)".into());
}
// 2) Only work inside the paths a capability allows
let app_data = app
.path()
.app_data_dir()
.map_err(|e| format!("no app data dir: {e}"))?;
let mut path: PathBuf = app_data.join("notes");
std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
path.push(format!("{}.json", note.id));
// 3) Serialize and write
let body = serde_json::to_vec_pretty(¬e).map_err(|e| e.to_string())?;
let bytes = body.len();
std::fs::write(&path, body).map_err(|e| e.to_string())?;
Ok(SaveResult {
bytes_written: bytes,
path: path.display().to_string(),
})
}
src-tauri/src/lib.rs:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![
commands::save_note,
])
.run(tauri::generate_context!())
.expect("tauri failed to start");
}
TypeScript side: the call
src/lib/notes.ts:
import { invoke } from '@tauri-apps/api/core'
export interface Note {
id: string
title: string
body: string
}
export interface SaveResult {
bytesWritten: number
path: string
}
export async function saveNote(note: Note): Promise<SaveResult> {
// invoke maps 1:1 to a Rust #[tauri::command] by name.
// Serialization is automatic (serde <-> JSON).
// Watch out for snake_case vs camelCase mapping rules.
const raw = await invoke<{ bytes_written: number; path: string }>('save_note', {
note,
})
return { bytesWritten: raw.bytes_written, path: raw.path }
}
In a React component:
import { useState } from 'react'
import { saveNote } from './lib/notes'
export function NoteEditor() {
const [title, setTitle] = useState('')
const [body, setBody] = useState('')
const [status, setStatus] = useState<string | null>(null)
async function onSave() {
try {
const result = await saveNote({
id: crypto.randomUUID(),
title,
body,
})
setStatus(`Saved: ${result.path} (${result.bytesWritten} bytes)`)
} catch (e) {
setStatus(`Failed: ${String(e)}`)
}
}
return (
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea value={body} onChange={(e) => setBody(e.target.value)} />
<button onClick={onSave}>Save</button>
{status && <p>{status}</p>}
</div>
)
}
Why this is safe
- Explicit capability — only windows whose capability includes
save_notecan call it - Rust input validation — slashes in the id are a directory-traversal attempt, size limits are explicit
- Path confinement — work happens under
app_data_dir()only; no user-controlled path injection - Type safety — the Rust compiler checks one side, invoke's generics check the other
Writing the same thing in Electron means assembling preload, contextBridge, IPC handlers, and explicit whitelists yourself. Roughly the same volume of code, but forgetting a piece becomes a security hole.
Summary: Tauri's IPC is enforced by Rust's type system and the capability system together. Security sits next to the code that does the work.
6. Mobile — the headline change in Tauri 2
Tauri 1 was desktop-only. The biggest banner on v2 was iOS and Android support. In 2026, the question is how far that has actually come.
What works
- iOS and Android builds are stable (since the 2.0 GA)
- The same web frontend is shared between desktop and mobile
- Rust backend logic is callable from mobile too; native plugins can be written in Swift/Kotlin/Java
- Many of the core plugins (filesystem, notifications, dialogs) work on mobile
What is still rough
- Desktop plugin set is not equal to mobile plugin set. Some plugins are desktop-only, some are mobile-only
- The Tauri team itself acknowledges the "mobile as a first-class citizen" promise was overstated — they shipped the foundation and are iterating with the community
- iOS sideloading remains constrained by Apple policy; Android is comparatively open
- Tauri does not match Flutter or React Native on native-feeling mobile UX out of the box — being a webview, you still need to invest in mobile look-and-feel
When to choose Tauri mobile
| Scenario | Recommendation |
|---|---|
| Existing Tauri desktop app, extend to mobile | Tauri mobile works well |
| Mobile-first app, desktop secondary | React Native / Flutter / native |
| Desktop and mobile both first-class, code sharing matters | Seriously evaluate Tauri |
| Native mobile UX is the product (gestures, transitions) | Native or Flutter |
Summary: Tauri mobile is excellent as "the mobile companion to a desktop app." It is not yet as mature as React Native or Flutter as the foundation for a mobile-first product.
7. Electron, Wails, Neutralino, native — the comparison matrix
Each tool in its place.
| Property | Tauri 2 | Electron | Wails | Neutralino | Native |
|---|---|---|---|---|---|
| Backend language | Rust | Node.js | Go | C/C++(core) | Swift/Kotlin/C++/C# |
| Rendering | System webview | Bundled Chromium | System webview | System webview | Native UI |
| Bundle size | 5-40MB | 80-200MB | 10-25MB | 2-10MB | 1-20MB |
| Memory idle | 30-80MB | 200-400MB | 50-100MB | 30-70MB | 20-80MB |
| Mobile | iOS/Android | None | None | None | First-class |
| Render consistency | Low (OS-dependent) | High | Low | Low | High |
| Learning curve | High (Rust) | Low (JS) | Medium (Go) | Low | Highest |
| Security model | capabilities (declarative) | Code patterns | Method bindings | allowList | OS APIs |
| Maturity | High (2024 GA) | Very high | Medium | Medium | Top |
| Ecosystem | Growing fast | Huge | Small | Small | OS-dependent |
| Sponsor | Foundation (independent) | OpenJS | Community | Community | Platform vendor |
Each tool in one line
- Tauri 2: lightweight, secure desktop and mobile. Best if you are willing to learn Rust.
- Electron: largest ecosystem, consistent rendering. Pay the weight for fast development.
- Wails: the natural choice for Go teams. Brings Go's simplicity to desktop. Some features (multi-window etc.) still in progress.
- Neutralino: ultra-light. No compilation, Node-like APIs. Lightest, but the least mature.
- Native: one team per platform. Best UX, most expensive.
8. The Rust learning curve — the real cost for a frontend team
The biggest barrier to Tauri is not bundle size, nor the system webview. It is Rust. Honest accounting of the cost when a frontend-heavy team picks it up.
The good news
- Tauri is "frontend as usual + Rust only for the backend." For a simple app with thin backend, the Rust footprint is a few IPC commands and a main file
- The Tauri CLI generates ~90% of the boilerplate — cargo, cross-compile, code-signing wiring are largely abstracted
- AI tooling (Claude, ChatGPT) is good at generating Rust. Simple IPC commands are mostly autocompleted
- A thin backend rarely touches the hard parts of Rust — lifetimes, deep async, unsafe
The bad news
- The moment the backend grows — DB pools, async work, concurrency — the Rust curve gets steep
- Compile times. Initial build 5-15 minutes, incremental 5-30 seconds. Electron has instant reload
- Error messages are friendly but voluminous. A one-line JS error becomes 30 lines of trait-bound explanations in Rust
- If exactly one person on the team can review Rust, that person becomes the bottleneck
- Dependency auditing.
cargo auditis fine, but the tooling surface is thinner than the npm world, and some crates lean heavily on unsafe
A practical decision rubric
| Team situation | Recommendation |
|---|---|
| Frontend-only team, no Rust experience, simple backend | Tauri worth trying |
| Frontend-only team, complex backend (DB, concurrency) | Electron + Node, or Tauri with sidecar |
| At least one Rust-experienced member | Tauri strongly recommended |
| Systems-programming background | Tauri feels natural |
| Must ship in six months, schedule tight | Electron — everything is paved |
A useful pattern: many teams compromise with the sidecar pattern. Keep Tauri's main app lean, spawn the heavy backend (e.g., Python ML, a Node service) as a separate process under Tauri's supervision. This minimizes the Rust learning cost while still capturing the bundle and memory wins.
Summary: Rust is a real cost for a frontend team. Bearable when the backend is simple. A different tool may be better when it isn't.
9. Apps actually built with Tauri 2
Notable Tauri 2 apps as of 2026. Names, categories, and a guess at why Tauri.
| App | Category | Why Tauri (likely) |
|---|---|---|
| SilentKeys | Privacy-first voice dictation | On-device ML, small bundle, deep system integration |
| Voxly | AI voice dictation | Fast startup, low memory, system tray |
| Watson.ai | Meeting capture and extraction | Background operation, fine-grained system permissions |
| Pake | Turn any webpage into a desktop app | Small bundle is the value proposition (clear win over Electron) |
| Rivet | Visual programming for AI agents | IDE-class UX, memory efficiency matters |
| XGetter | Video / audio downloader | System integration, small installer |
| Kunobi / Kubeli | Kubernetes desktop clients | Developer tooling, fast startup, MCP server built in |
Patterns you can see:
- New projects — built on Tauri from scratch, not ports from Electron
- Small to medium scope — not gargantuan IDEs, but tools with a clear single purpose
- System integration is central — tray icons, background work, global shortcuts, fine system permissions
- Memory-sensitive — apps that run continuously in the background (dictation, meeting capture)
- Developer or power-user audience — non-trivial Linux share
Conversely, why migrations from Electron are rare:
- The Electron codebase is a large existing asset — rewriting the backend in Rust is expensive
- Users are already happy — don't fix what isn't broken
- A low Linux share makes the system-webview gap easier to ignore
Summary: Tauri overwhelmingly wins as a deliberate choice in new projects. Migrations from existing Electron apps need a strong, specific motivation.
10. When to pick what — a decision tree
Start:
│
├─ Is mobile a primary target?
│ ├─ Yes → React Native / Flutter / native
│ └─ No → continue
│
├─ Is Linux a serious target?
│ ├─ Yes, 30%+ Linux users → can you absorb the system-webview gap?
│ │ ├─ Yes → Tauri / Wails
│ │ └─ No, pixel consistency required → Electron
│ └─ No or small → continue
│
├─ Does the team have Rust experience?
│ ├─ Yes or willing to learn → Tauri strongly recommended
│ ├─ Go experience instead → consider Wails
│ └─ JS only, no learning time → Electron
│
├─ Are bundle size and memory truly important?
│ ├─ Yes (background app, low-spec hardware) → Tauri
│ └─ No, users don't care → Electron ships faster
│
├─ Is the backend complex?
│ ├─ Yes — heavy DB, concurrency → Electron, or Tauri with sidecar
│ └─ No, mostly UI → Tauri or Neutralino
│
└─ Tight schedule (3-6 months)?
├─ Yes → Electron (everything is paved)
└─ No, 12+ months → Tauri's learning cost amortizes
Short rules
- New project, simple backend, bundle matters → Tauri
- Existing Electron app, users happy → leave it
- AAA-class desktop UX with Linux as a first-class citizen → Electron
- Desktop + mobile code sharing matters → Tauri or Flutter
- Go team → Wails
- Need to claw back every megabyte → Neutralino (at the cost of maturity)
- Per-OS best UX, unlimited headcount → native
11. Anti-patterns and pitfalls
Patterns that lead to regretting the Tauri choice.
"Write it like Electron"
- Symptom: a single huge Rust file with every IPC command in one place
- Result: compile times explode, every change is expensive, tests are impossible
- Instead: split the Rust into modules (
commands/,services/,models/). Keep commands thin (validate + delegate to a service).
"Turn on every capability"
- Symptom:
core:defaultplus every fs/shell/dialog permission with wildcards - Result: the core value of the security model evaporates. You inherit Electron's risk surface
- Instead: only what is truly needed, with narrow scopes. Per-window capabilities.
"Go deep on Rust async from day one"
- Symptom: simple IPC implemented with
tokio::spawnand six channels - Result: learning curve explodes, deadlocks, a swamp of compile errors
- Instead: start with
async fnon Tauri's default runtime. Reach for deeper concurrency only when the problem demands it.
"Ignore the system-webview gap"
- Symptom: develop only on macOS, test on Linux right before release
- Result: half the UI breaks under WebKitGTK
- Instead: CI on three OSes from week one. Visual regression on every critical screen across all targets.
"Use Rust for the frontend too"
- Symptom: Leptos / Yew / Dioxus across the whole stack
- Result: the Rust curve doubles; the ecosystem is one-hundredth of React's
- Instead: stay on your usual framework (React/Vue/Svelte); Rust for the backend only. Tauri is designed for that split.
"Ask an AI agent to build the whole Tauri app"
- Symptom: an agent ships capability configs and IPC patterns that look right but leak permissions
- Result: too-wide permissions, missing input validation, side channels opening
- Instead: a human reviews every capability and IPC command. Tests at the IPC boundary are mandatory, not optional.
12. Migration — when moving from Electron to Tauri
If a migration is justified, here is the sequence.
1. Measure: record current Electron bundle, memory, startup
2. Backend inventory: list every Node dependency — what maps to a Rust crate?
3. Frontend pull-out: can the UI move unchanged (usually yes)?
4. IPC mapping: every ipcMain handler maps to a Tauri command
5. Capability design: list what each IPC touches → capabilities files
6. System integration: tray / notifications / shortcuts via Tauri plugins
7. Cross-OS testing: three-OS CI on day one, visual regression
8. Re-measure: verify the promised numbers actually appear
9. Beta users → gradual rollout
Signals not to migrate
- Many Node dependencies have no Rust equivalent (specific ML libraries, etc.)
- 70% of users on macOS, system-webview gap is moot, memory is fine
- Zero Rust-capable engineers, no six months to learn
- Users are happy, no complaints about bundle size
Signals to migrate
- A 24/7 background app whose memory is an actual user-machine burden
- Installer size affects download conversion (low-spec, low-bandwidth markets)
- A security audit is on the docket and cleaning up Electron's surface costs more
- A mobile expansion is on the roadmap and you want desktop code reuse
Epilogue — checklist, anti-patterns, what's next
Tauri 2 is not "Electron made lighter." It is a different tool with different trade-offs. Accept the system-webview shadow, the Rust curve, the smaller ecosystem — get back bundle, memory, and a stronger default security model. New project, thin backend, bundle matters — Tauri is the rational choice. Existing Electron apps need a real reason before any migration.
Checklist for choosing Tauri
- Is this a new project or an existing migration? — new project amortizes the cost
- How complex is the backend? — simpler favors Tauri
- Mobile plans? — yes makes Tauri 2's mobile support attractive
- Rust experience on the team? — zero means add learning time to the schedule
- Linux user share? — high means budget for system-webview QA
- Do bundle and memory truly matter? — background apps and low-spec hardware win here
- Schedule pressure? — tight favors Electron; 12+ months favors Tauri
- Designed capabilities early? — the security model shapes the code layout
- Sidecar pattern needed? — heavy backends should run as a separate process
- Three-OS CI from week one? — discovering the gap at release is too late
Anti-patterns
| Anti-pattern | Why it's bad | Instead |
|---|---|---|
| Choosing Tauri only on bundle size | Ignores Rust and QA cost | Score team and schedule too |
| 1:1 porting an Electron codebase to Rust | Rust idioms differ | Redesign per domain |
| Wildcard capabilities | Security model collapses | Only what's needed, narrow scopes |
| Linux testing only at the end | Found right before launch | Three-OS CI from week one |
| Rust for the frontend (Leptos etc.) | One-hundredth the ecosystem | Frontend as usual, Rust for backend only |
One giant commands.rs | Compile times explode | Split by domain into modules |
| Deep async from day one | Curve becomes a cliff | Sync first, async when needed |
| Assume webview consistency | Ship breaks on macOS-only review | CSS/JS fallbacks plus visual regression on all OSes |
| Expect Tauri mobile to replace React Native | Mobile-first UX still maturing | Use as a desktop companion |
| Migrate without ROI | Wastes assets, disturbs users | Only when measurable wins exist |
What's next
Next: "Pitfalls of Rust async — async/await, tokio, Send bounds, and where lifetimes meet futures." If this post was about deciding whether to use Tauri, the next is about what you actually meet when the Tauri backend gets complex. Rust async is powerful but full of traps — Send/Sync trait bounds, the interaction between lifetimes and futures, channel choice (mpsc/oneshot/broadcast), select patterns, and how to call sync code from an async context. It applies to a Tauri backend, a backend microservice, anywhere Rust async is taken seriously.
References
- Tauri 2.0 Stable Release announcement
- Tauri official site
- Tauri architecture documentation
- Tauri security model overview
- Tauri Capabilities reference
- Tauri Permissions guide
- Tauri Mobile Alpha announcement
- Tauri GitHub repository
- Awesome Tauri — apps and plugins index
- Made with Tauri — showcase
- Electron official site
- Wails official site
- Wails vs Tauri discussion
- Neutralino.js official
- Tauri 1.3 — early allowlist context
- Roadmap to Tauri 2.0
- Tauri vs Electron — performance and trade-offs (Hopp)
- Tauri isolation pattern
- Evil Martians — Tauri sidecar pattern