프롤로그 — 왜 다시 데스크톱 프레임워크 이야기인가
"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 사이드카 패턴
현재 단락 (1/405)
"Electron은 무겁다"는 말은 너무 자주 들어서 이제 클리셰가 됐다. Slack을 띄우면 RAM 400MB가 사라지고, VS Code는 1GB까지 올라가고, 디스코드 인스톨러...