Tauri 완전 가이드 | Electron보다 가볍고 빠른 데스크톱 앱 개발
이 글의 핵심
Electron의 강력한 대안 Tauri. Rust 기반으로 번들 크기는 10MB 이하, 메모리는 1/3만 사용하면서도 Windows·macOS·Linux를 모두 지원합니다. 웹 기술로 네이티브급 성능의 데스크톱 앱을 만드세요.
이 글의 핵심
Tauri는 Electron보다 10배 가볍고 빠른 크로스플랫폼 데스크톱 앱 프레임워크입니다. Rust 기반으로 번들 크기는 10MB 이하, 메모리는 Electron의 1/3만 사용하면서도 React·Vue·Svelte 등 익숙한 웹 기술로 개발할 수 있습니다.
목차
Tauri란?
Tauri는 2019년 시작된 오픈소스 프로젝트로, Rust + Web 기술로 네이티브 데스크톱 앱을 만드는 프레임워크입니다.
🚀 핵심 특징
1. 압도적으로 가벼움
Electron 앱:
- 번들 크기: 100MB+
- 메모리 사용: 150MB+
- Chromium 포함
Tauri 앱:
- 번들 크기: 10MB 이하
- 메모리 사용: 50MB 이하
- 시스템 WebView 사용
2. 크로스플랫폼
- Windows (WebView2)
- macOS (WKWebView)
- Linux (WebKitGTK)
- iOS/Android (Tauri Mobile - 베타)
3. 강력한 보안
// 모든 API는 명시적 허용 필요
{
"tauri": {
"allowlist": {
"fs": {
"readFile": true,
"writeFile": false // 명시적 거부
}
}
}
}
4. 웹 기술 사용
- React, Vue, Svelte, Angular 모두 지원
- HTML/CSS/JavaScript로 UI 구성
- Rust로 백엔드 로직 작성
Tauri 시작하기
1️⃣ 사전 요구사항
Windows
# Microsoft C++ Build Tools 설치
# https://visualstudio.microsoft.com/downloads/
# WebView2 (Windows 10+ 기본 포함)
macOS
# Xcode Command Line Tools 설치
xcode-select --install
# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Linux (Ubuntu/Debian)
sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
build-essential \
curl \
wget \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev
# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2️⃣ 프로젝트 생성
# Tauri CLI 설치
npm install -g @tauri-apps/cli
# 새 프로젝트 생성 (React 템플릿)
npm create tauri-app@latest
# 프로젝트 이름: my-tauri-app
# 프론트엔드: React
# TypeScript: Yes
# Package Manager: npm
cd my-tauri-app
npm install
3️⃣ 프로젝트 구조
my-tauri-app/
├── src/ # React 프론트엔드
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/ # Rust 백엔드
│ ├── src/
│ │ └── main.rs # Rust 진입점
│ ├── tauri.conf.json # Tauri 설정
│ └── Cargo.toml # Rust 의존성
└── package.json
개발 서버 실행
# 개발 모드 (핫 리로드)
npm run tauri dev
# 빌드 (배포용)
npm run tauri build
Tauri API 사용하기
📁 파일 시스템 읽기
프론트엔드 (React)
// src/App.tsx
import { open } from '@tauri-apps/api/dialog';
import { readTextFile } from '@tauri-apps/api/fs';
import { useState } from 'react';
function App() {
const [content, setContent] = useState('');
const handleOpenFile = async () => {
try {
// 파일 선택 대화상자
const selected = await open({
multiple: false,
filters: [{
name: 'Text',
extensions: ['txt', 'md']
}]
});
if (selected) {
// 파일 읽기
const text = await readTextFile(selected as string);
setContent(text);
}
} catch (error) {
console.error(error);
}
};
return (
<div>
<button onClick={handleOpenFile}>Open File</button>
<pre>{content}</pre>
</div>
);
}
export default App;
권한 설정 (src-tauri/tauri.conf.json)
{
"tauri": {
"allowlist": {
"dialog": {
"open": true
},
"fs": {
"readFile": true,
"scope": ["**"]
}
}
}
}
💾 파일 저장
import { save } from '@tauri-apps/api/dialog';
import { writeTextFile } from '@tauri-apps/api/fs';
const handleSaveFile = async () => {
try {
const filePath = await save({
filters: [{
name: 'Text',
extensions: ['txt']
}]
});
if (filePath) {
await writeTextFile(filePath, content);
console.log('File saved!');
}
} catch (error) {
console.error(error);
}
};
🔔 시스템 알림
import { sendNotification } from '@tauri-apps/api/notification';
const handleNotify = async () => {
await sendNotification({
title: 'Tauri App',
body: 'Hello from Tauri!',
});
};
🪟 윈도우 제어
import { appWindow } from '@tauri-apps/api/window';
// 윈도우 최소화
await appWindow.minimize();
// 윈도우 최대화
await appWindow.maximize();
// 윈도우 닫기
await appWindow.close();
// 윈도우 숨기기
await appWindow.hide();
// 윈도우 크기 변경
await appWindow.setSize({ width: 800, height: 600 });
// 윈도우 타이틀 변경
await appWindow.setTitle('My App');
Rust 백엔드 함수 호출
1️⃣ Rust 함수 정의
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// 프론트엔드에서 호출할 함수
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
// HTTP 요청 (예시)
match reqwest::get(&url).await {
Ok(response) => match response.text().await {
Ok(body) => Ok(body),
Err(e) => Err(e.to_string()),
},
Err(e) => Err(e.to_string()),
}
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
greet,
add_numbers,
fetch_data
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2️⃣ 프론트엔드에서 호출
// src/App.tsx
import { invoke } from '@tauri-apps/api/tauri';
import { useState } from 'react';
function App() {
const [result, setResult] = useState('');
const handleGreet = async () => {
const message = await invoke<string>('greet', { name: 'Alice' });
setResult(message);
};
const handleAdd = async () => {
const sum = await invoke<number>('add_numbers', { a: 10, b: 20 });
setResult(`Sum: ${sum}`);
};
const handleFetch = async () => {
try {
const data = await invoke<string>('fetch_data', {
url: 'https://api.github.com/users/tauri-apps'
});
setResult(data);
} catch (error) {
setResult(`Error: ${error}`);
}
};
return (
<div>
<button onClick={handleGreet}>Greet</button>
<button onClick={handleAdd}>Add</button>
<button onClick={handleFetch}>Fetch</button>
<div>{result}</div>
</div>
);
}
실전 프로젝트: 메모장 앱
기능 명세
- ✅ 파일 열기/저장
- ✅ 자동 저장
- ✅ 탭 지원
- ✅ 다크 모드
- ✅ 찾기/바꾸기
Rust 백엔드 (자동 저장)
// src-tauri/src/main.rs
use std::fs;
use std::path::Path;
#[tauri::command]
fn auto_save(path: String, content: String) -> Result<(), String> {
fs::write(&path, content)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn get_recent_files() -> Vec<String> {
// 최근 파일 목록 (실제로는 DB나 파일에 저장)
vec![
"/path/to/file1.txt".to_string(),
"/path/to/file2.txt".to_string(),
]
}
React 프론트엔드
// src/App.tsx
import { useState, useEffect } from 'react';
import { open, save } from '@tauri-apps/api/dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import { invoke } from '@tauri-apps/api/tauri';
interface Tab {
id: string;
path: string | null;
content: string;
saved: boolean;
}
function App() {
const [tabs, setTabs] = useState<Tab[]>([
{ id: '1', path: null, content: '', saved: true }
]);
const [activeTab, setActiveTab] = useState('1');
// 자동 저장 (2초마다)
useEffect(() => {
const interval = setInterval(async () => {
const tab = tabs.find(t => t.id === activeTab);
if (tab && tab.path && !tab.saved) {
try {
await invoke('auto_save', {
path: tab.path,
content: tab.content
});
updateTab(activeTab, { saved: true });
} catch (error) {
console.error('Auto-save failed:', error);
}
}
}, 2000);
return () => clearInterval(interval);
}, [tabs, activeTab]);
const handleOpen = async () => {
const selected = await open({
multiple: false,
filters: [{ name: 'Text', extensions: ['txt', 'md'] }]
});
if (selected) {
const content = await readTextFile(selected as string);
const newTab: Tab = {
id: Date.now().toString(),
path: selected as string,
content,
saved: true
};
setTabs([...tabs, newTab]);
setActiveTab(newTab.id);
}
};
const handleSave = async () => {
const tab = tabs.find(t => t.id === activeTab);
if (!tab) return;
const filePath = tab.path || await save({
filters: [{ name: 'Text', extensions: ['txt'] }]
});
if (filePath) {
await writeTextFile(filePath, tab.content);
updateTab(activeTab, { path: filePath, saved: true });
}
};
const updateTab = (id: string, updates: Partial<Tab>) => {
setTabs(tabs.map(t => t.id === id ? { ...t, ...updates } : t));
};
const currentTab = tabs.find(t => t.id === activeTab);
return (
<div className="app">
<div className="toolbar">
<button onClick={handleOpen}>Open</button>
<button onClick={handleSave}>Save</button>
<button onClick={() => setTabs([...tabs, {
id: Date.now().toString(),
path: null,
content: '',
saved: true
}])}>New Tab</button>
</div>
<div className="tabs">
{tabs.map(tab => (
<div
key={tab.id}
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.path ? tab.path.split('/').pop() : 'Untitled'}
{!tab.saved && ' *'}
<button onClick={() => setTabs(tabs.filter(t => t.id !== tab.id))}>
×
</button>
</div>
))}
</div>
{currentTab && (
<textarea
value={currentTab.content}
onChange={(e) => updateTab(activeTab, {
content: e.target.value,
saved: false
})}
placeholder="Start typing..."
/>
)}
</div>
);
}
export default App;
빌드 및 배포
개발 빌드
npm run tauri build
결과물:
- Windows:
.exe(src-tauri/target/release) - macOS:
.app,.dmg(src-tauri/target/release/bundle) - Linux:
.deb,.AppImage(src-tauri/target/release/bundle)
릴리스 최적화
// src-tauri/tauri.conf.json
{
"tauri": {
"bundle": {
"identifier": "com.myapp.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [],
"copyright": "",
"category": "Utility",
"shortDescription": "My awesome app",
"longDescription": "Detailed description..."
}
}
}
자동 업데이트
// src-tauri/src/main.rs
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let handle = app.handle();
tauri::async_runtime::spawn(async move {
// 업데이트 확인
match handle.updater().check().await {
Ok(update) => {
if update.is_update_available() {
update.download_and_install().await.unwrap();
}
}
Err(e) => println!("Failed to check for updates: {}", e),
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Tauri vs Electron
| 항목 | Tauri | Electron |
|---|---|---|
| 번들 크기 | 10MB 이하 | 100MB+ |
| 메모리 | 50MB | 150MB+ |
| 시작 시간 | <1초 | 2-3초 |
| 언어 | Rust + Web | JavaScript + Web |
| WebView | 시스템 기본 | Chromium 내장 |
| 보안 | 기본적으로 강력 | 추가 설정 필요 |
| 크로스플랫폼 | ✅ | ✅ |
| 생태계 | 🌱 성장 중 | 🌳 성숙 |
Tauri Mobile (iOS/Android)
모바일 앱 개발 (베타)
# iOS 타겟 추가
npm run tauri ios init
npm run tauri ios dev
# Android 타겟 추가
npm run tauri android init
npm run tauri android dev
하나의 코드베이스로 5개 플랫폼 지원:
- Windows
- macOS
- Linux
- iOS
- Android
실제 사용 사례
1. VS Code 대체 에디터
- Zed: 고성능 코드 에디터
- Lapce: Rust 기반 에디터
2. 개발 도구
- DevToys: 개발자 유틸리티 모음
- Clash Verge: 네트워크 프록시 GUI
3. 생산성 도구
- Pomatez: 포모도로 타이머
- Icalingua: 채팅 클라이언트
자주 묻는 질문 (FAQ)
Q1. Tauri 앱의 실제 성능은 어떤가요?
A: 벤치마크 결과:
- 시작 시간: Electron 2.5초 → Tauri 0.8초
- 메모리 사용: Electron 150MB → Tauri 45MB
- 번들 크기: Electron 120MB → Tauri 8MB
Q2. 시스템 WebView를 사용하면 브라우저 차이 문제가 있지 않나요?
A: 맞습니다. 하지만:
- Windows: WebView2 (Chromium 기반, 자동 업데이트)
- macOS: WKWebView (Safari 기반, 최신 표준 지원)
- Linux: WebKitGTK (최신 버전 권장)
대부분의 모던 웹 기술은 문제없이 작동합니다.
Q3. Rust를 모르는데 Tauri를 배울 수 있나요?
A: 네! 대부분의 로직은 프론트엔드(React/Vue)에서 작성하고, Rust는:
- 템플릿 코드 사용 (복붙만 해도 됨)
- 필요한 경우에만 커스텀 함수 작성
- ChatGPT로 Rust 코드 생성 가능
Q4. Electron에서 Tauri로 마이그레이션할 수 있나요?
A: 가능하지만 노력이 필요합니다.
- UI 코드: 그대로 사용 가능
- Node.js API: Tauri API로 재작성 필요
- 네이티브 모듈: Rust로 재작성
Q5. Tauri의 단점은 무엇인가요?
A:
- 생태계: Electron보다 작음 (하지만 빠르게 성장 중)
- Rust 학습 곡선: 고급 기능은 Rust 지식 필요
- 디버깅: Electron보다 복잡할 수 있음
핵심 정리
✅ Tauri의 장점
- 압도적으로 가벼움: 번들 10MB, 메모리 50MB
- 빠른 성능: 네이티브급 시작 시간
- 강력한 보안: 기본적으로 안전한 설계
- 웹 기술 사용: React/Vue/Svelte
- 크로스플랫폼: Windows/macOS/Linux/iOS/Android
🚀 다음 단계
- Tauri 공식 문서에서 심화 학습
- Tauri GitHub에서 소스 코드 탐색
- Tauri Discord에서 커뮤니티 참여
시작하기:
npm create tauri-app@latest로 5분 만에 프로젝트를 시작하고, Electron보다 10배 가벼운 데스크톱 앱을 만드세요! 🚀