Tauri v2 아키텍처 가이드
작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Tauri v2 데스크탑 앱 (Windows / macOS / Linux) 공통 참조:
../common/(프로젝트 구조, 보안, 인증, 테스트 등)
목차
- Tauri v2 아키텍처 개요
- IPC 통신
- Capabilities & Permissions
- Plugin 시스템
- Multi-Window / Multi-WebView
- 시스템 트레이
- 메뉴 바
- 프론트엔드 구조
- Sidecar (외부 바이너리 번들링)
1. Tauri v2 아키텍처 개요
핵심 구성요소
+-------------------------------------------------------+
| Tauri Application |
| |
| +------------------+ +----------------------+ |
| | Rust Core | | WebView (Frontend) | |
| | | IPC | | |
| | - Commands |<---->| - TypeScript/JS | |
| | - Events | | - SvelteKit/React | |
| | - State | | - HTML/CSS | |
| | - Plugins | | | |
| +------------------+ +----------------------+ |
| | | |
| +--------+--------+ +--------+--------+ |
| | OS Native APIs | | Tao (Window) | |
| | - Filesystem | | Wry (WebView) | |
| | - Network | | | |
| | - Process | | - Windows: WebView2 |
| | - Keychain | | - macOS: WKWebView |
| | - Notifications | | - Linux: WebKitGTK |
| +-----------------+ +-----------------+ |
+-------------------------------------------------------+
Tauri v1 vs v2 주요 차이
| 항목 | v1 | v2 |
|---|---|---|
| 보안 모델 | allowlist (boolean 토글) | Capabilities + Permissions + Scopes |
| 모바일 지원 | 없음 | iOS / Android 지원 |
| 플러그인 시스템 | 기본 | ACL 기반 권한 내장 |
| IPC | invoke + listen | invoke + events + channels |
| Multi-WebView | 미지원 | WebView 단위 권한 분리 |
| 빌드 시스템 | tauri.conf.json 단일 | tauri.conf.json + capabilities/*.json 분리 |
프로젝트 구조
폴더 구조 상세는
/platform/client/common/project-structure참조
tauri-app/
+-- src-tauri/ # Rust 백엔드
| +-- src/
| | +-- lib.rs # 앱 빌더 + 플러그인 등록
| | +-- commands/ # IPC 커맨드 핸들러
| | | +-- mod.rs
| | | +-- session.rs
| | | +-- filesystem.rs
| | +-- services/ # 비즈니스 로직 (Rust)
| | +-- state/ # 앱 상태 관리
| | +-- tray.rs # 시스템 트레이
| | +-- menu.rs # 메뉴 바
| +-- Cargo.toml
| +-- tauri.conf.json # Tauri 설정
| +-- capabilities/ # 권한 정의 (v2 신규)
| | +-- default.json
| | +-- admin.json
| +-- icons/ # 앱 아이콘
| +-- build.rs
+-- src/ # 프론트엔드
| +-- lib/ # SvelteKit 또는 React
| +-- routes/ # 페이지
| +-- app.html
+-- package.json
+-- vite.config.ts
+-- tsconfig.json
앱 초기화 (lib.rs)
// src-tauri/src/lib.rs
use tauri::Manager;
mod commands;
mod state;
mod tray;
mod menu;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
// 플러그인 등록
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
// 상태 관리
.manage(state::AppState::default())
// IPC 커맨드 등록
.invoke_handler(tauri::generate_handler![
commands::session::create_session,
commands::session::send_message,
commands::filesystem::read_config,
])
// 시스템 트레이
.setup(|app| {
tray::create_tray(app)?;
menu::setup_menu(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2. IPC 통신
Tauri v2는 세 가지 IPC 메커니즘을 제공한다.
IPC 메커니즘 비교
| 메커니즘 | 방향 | 용도 | 패턴 |
|---|---|---|---|
| invoke | Frontend -> Rust | 명령 실행 + 결과 반환 | Request-Response |
| events | 양방향 | 비동기 알림, 브로드캐스트 | Pub-Sub |
| channels | Rust -> Frontend | 스트리밍 데이터 (진행률 등) | Streaming (v2 신규) |
2.1 invoke (커맨드 호출)
// src-tauri/src/commands/session.rs
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::state::AppState;
#[derive(Serialize, Deserialize)]
pub struct SessionInfo {
pub session_id: String,
pub created_at: String,
}
#[tauri::command]
pub async fn create_session(
state: State<'_, AppState>,
model: String,
) -> Result<SessionInfo, String> {
let session = state
.session_manager
.lock()
.await
.create(model)
.map_err(|e| e.to_string())?;
Ok(SessionInfo {
session_id: session.id.clone(),
created_at: session.created_at.to_rfc3339(),
})
}
// src/lib/api/session.ts
import { invoke } from '@tauri-apps/api/core';
interface SessionInfo {
session_id: string;
created_at: string;
}
export async function createSession(model: string): Promise<SessionInfo> {
return await invoke<SessionInfo>('create_session', { model });
}
2.2 events (이벤트)
// Rust -> Frontend 이벤트 발행
use tauri::Emitter;
fn notify_status_change(app: &tauri::AppHandle, status: &str) {
app.emit("session-status-changed", status)
.expect("failed to emit event");
}
// 특정 윈도우에만 발행
fn notify_window(window: &tauri::WebviewWindow, payload: &str) {
window.emit("window-specific-event", payload)
.expect("failed to emit to window");
}
// Frontend에서 이벤트 수신
import { listen, emit } from '@tauri-apps/api/event';
// Rust -> Frontend 수신
const unlisten = await listen<string>('session-status-changed', (event) => {
console.log('Status:', event.payload);
});
// Frontend -> Rust 이벤트 발행
await emit('user-action', { action: 'click', target: 'button' });
// 정리 (컴포넌트 언마운트 시)
unlisten();
2.3 channels (스트리밍, v2 신규)
channels는 Rust에서 Frontend로 대량의 데이터를 스트리밍할 때 사용한다. invoke의 반환값보다 효율적이다.
// Rust: 채널로 진행률 스트리밍
use tauri::ipc::Channel;
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
pub enum DownloadProgress {
#[serde(rename_all = "camelCase")]
Started { content_length: u64 },
#[serde(rename_all = "camelCase")]
Progress { chunk_length: u64, downloaded: u64 },
Finished,
}
#[tauri::command]
pub async fn download_file(
url: String,
on_progress: Channel<DownloadProgress>,
) -> Result<String, String> {
let total_size = 1024 * 1024; // 예시
on_progress.send(DownloadProgress::Started {
content_length: total_size,
}).map_err(|e| e.to_string())?;
let mut downloaded: u64 = 0;
// ... 다운로드 로직 ...
for chunk in chunks {
downloaded += chunk.len() as u64;
on_progress.send(DownloadProgress::Progress {
chunk_length: chunk.len() as u64,
downloaded,
}).map_err(|e| e.to_string())?;
}
on_progress.send(DownloadProgress::Finished)
.map_err(|e| e.to_string())?;
Ok("download complete".to_string())
}
// Frontend: 채널 수신
import { invoke, Channel } from '@tauri-apps/api/core';
type DownloadProgress =
| { event: 'Started'; data: { contentLength: number } }
| { event: 'Progress'; data: { chunkLength: number; downloaded: number } }
| { event: 'Finished' };
const onProgress = new Channel<DownloadProgress>();
onProgress.onmessage = (message) => {
if (message.event === 'Progress') {
const percent = (message.data.downloaded / totalSize) * 100;
updateProgressBar(percent);
}
};
await invoke('download_file', { url: 'https://...', onProgress });
IPC 설계 체크리스트
- 모든
#[tauri::command]에 입력값 검증 구현 - 에러를
Result<T, String>또는 커스텀 에러 타입으로 반환 - 무거운 작업은
async+tokio::spawn으로 비동기 처리 - 스트리밍 데이터는 events 대신 channels 사용
- 커맨드 이름은 snake_case, 프론트엔드 호출 시에도 동일
-
State<T>로 앱 상태 공유 (Mutex/RwLock 사용)
3. Capabilities & Permissions
Tauri v2의 보안 모델은 Capabilities > Permissions > Scopes 3계층으로 구성된다.
보안 모델 구 조
Capability (역할 단위)
+-- 어떤 Window/WebView에 적용?
+-- Permission (기능 단위)
+-- allow / deny (명시적)
+-- Scope (범위 제한)
+-- 허용 URL, 파일 경로 등
capabilities/ 폴더 구성
src-tauri/capabilities/
+-- default.json # 기본 윈도우 권한
+-- admin.json # 관리자 전용 권한 (확장)
+-- minimal.json # 최소 권한 (제한된 WebView)
기본 Capability 설정
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "메인 윈도우 기본 권한",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"notification:default",
"clipboard-manager:allow-write",
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.example.com/**" },
{ "url": "https://auth.example.com/**" }
],
"deny": [
{ "url": "http://**" }
]
},
{
"identifier": "fs:allow-read",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$APPCONFIG/**" }
]
},
{
"identifier": "fs:allow-write",
"allow": [
{ "path": "$APPDATA/**" }
]
},
{
"identifier": "shell:allow-open",
"allow": [
{ "cmd": "open", "args": ["https://*"] }
]
}
]
}
주요 Scope 변수
| 변수 | 설명 | 예시 경로 (macOS) |
|---|---|---|
$APPDATA | 앱 데이터 폴더 | ~/Library/Application Support/{bundle_id} |
$APPCONFIG | 앱 설정 폴더 | ~/Library/Application Support/{bundle_id} |
$APPLOCALDATA | 앱 로컬 데이터 | ~/Library/Application Support/{bundle_id} |
$APPCACHE | 앱 캐시 | ~/Library/Caches/{bundle_id} |
$APPLOG | 앱 로그 | ~/Library/Logs/{bundle_id} |
$HOME | 홈 디렉토리 | ~/ |
$DESKTOP | 바탕화면 | ~/Desktop |
$DOCUMENT | 문서 폴더 | ~/Documents |
$DOWNLOAD | 다운로드 폴더 | ~/Downloads |
$TEMP | 임시 폴더 | /tmp |
권한 설정 원칙
| 원칙 | 설명 |
|---|---|
| 최소 권한 | 필요한 기능만 허용 (deny가 allow보다 우선) |
| 윈도우 분리 | 각 윈도우/WebView에 별도 Capability 적용 |
| Scope 제한 | URL, 파일 경로를 최소한으로 지정 |
| 명시적 허용 | 위험 권한(shell:allow-execute 등)은 반드시 명시적 허용 |
4. Plugin 시스템
공식 플러그인 목록
| 플러그인 | 패키지 | 용도 |
|---|---|---|
tauri-plugin-updater | @tauri-apps/plugin-updater | 자동 업데이트 |
tauri-plugin-shell | @tauri-apps/plugin-shell | 쉘 명령/sidecar 실행 |
tauri-plugin-store | @tauri-apps/plugin-store | Key-Value 영구 저장소 |
tauri-plugin-fs | @tauri-apps/plugin-fs | 파일시스템 접근 |
tauri-plugin-http | @tauri-apps/plugin-http | HTTP 클라이언트 |
tauri-plugin-notification | @tauri-apps/plugin-notification | OS 알림 |
tauri-plugin-dialog | @tauri-apps/plugin-dialog | 파일 선택/저장 다이얼로그 |
tauri-plugin-clipboard-manager | @tauri-apps/plugin-clipboard-manager | 클립보드 |
tauri-plugin-process | @tauri-apps/plugin-process | 프로세스 재시작/종료 |
tauri-plugin-os | @tauri-apps/plugin-os | OS 정보 |
tauri-plugin-log | @tauri-apps/plugin-log | 로깅 |
tauri-plugin-sql | @tauri-apps/plugin-sql | SQLite/MySQL/PostgreSQL |
tauri-plugin-global-shortcut | @tauri-apps/plugin-global-shortcut | 글로벌 단축키 |
tauri-plugin-deep-link | @tauri-apps/plugin-deep-link | Deep Link / Custom Protocol |
tauri-plugin-autostart | @tauri-apps/plugin-autostart | OS 시작 시 자동 실행 |
tauri-plugin-window-state | @tauri-apps/plugin-window-state | 윈도우 위치/크기 저장 |
tauri-plugin-single-instance | @tauri-apps/plugin-single-instance | 단일 인스턴스 |
플러그인 등록 패턴
// Cargo.toml
[dependencies]
tauri-plugin-store = "2"
tauri-plugin-fs = "2"
tauri-plugin-http = "2"
tauri-plugin-notification = "2"
tauri-plugin-shell = "2"
tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-process = "2"
tauri-plugin-log = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-window-state = "2"
# Frontend 패키지 설치
npm install @tauri-apps/plugin-store @tauri-apps/plugin-fs \
@tauri-apps/plugin-http @tauri-apps/plugin-notification \
@tauri-apps/plugin-shell @tauri-apps/plugin-updater \
@tauri-apps/plugin-dialog @tauri-apps/plugin-clipboard-manager \
@tauri-apps/plugin-process @tauri-apps/plugin-log
커스텀 플러그인 작성
// src-tauri/src/plugins/analytics.rs
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
};
#[tauri::command]
async fn track_event(event_name: String, properties: serde_json::Value) -> Result<(), String> {
// 분석 이벤트 전송 로직
println!("Track: {} {:?}", event_name, properties);
Ok(())
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("analytics")
.invoke_handler(tauri::generate_handler![track_event])
.setup(|app, _api| {
// 플러그인 초기화
Ok(())
})
.build()
}
// lib.rs에서 등록
.plugin(plugins::analytics::init())
5. Multi-Window / Multi-WebView
윈도우 생성
// Rust에서 새 윈도우 생성
use tauri::Manager;
#[tauri::command]
async fn open_settings_window(app: tauri::AppHandle) -> Result<(), String> {
// 이미 열려있으면 포커스
if let Some(window) = app.get_webview_window("settings") {
window.set_focus().map_err(|e| e.to_string())?;
return Ok(());
}
// 새 윈도우 생성
let _window = tauri::WebviewWindowBuilder::new(
&app,
"settings",
tauri::WebviewUrl::App("/settings".into()),
)
.title("Settings")
.inner_size(600.0, 400.0)
.resizable(true)
.center()
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
// Frontend에서 새 윈도우 생성
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
const settingsWindow = new WebviewWindow('settings', {
url: '/settings',
title: 'Settings',
width: 600,
height: 400,
center: true,
resizable: true,
});
settingsWindow.once('tauri://created', () => {
console.log('Window created');
});
settingsWindow.once('tauri://error', (e) => {
console.error('Window creation error:', e);
});
윈도우 간 통신
// 윈도우 A에서 윈도우 B로 이벤트 전송
import { emit, listen } from '@tauri-apps/api/event';
// 모든 윈도우에 브로드캐스트
await emit('theme-changed', { theme: 'dark' });
// 특정 윈도우에서 수신
const unlisten = await listen<{ theme: string }>('theme-changed', (event) => {
applyTheme(event.payload.theme);
});
윈도우별 Capability 분리
// capabilities/settings-window.json
{
"identifier": "settings-window",
"description": "설정 윈도우 - 파일시스템 접근 추가",
"windows": ["settings"],
"permissions": [
"core:default",
"dialog:allow-open",
{
"identifier": "fs:allow-read",
"allow": [{ "path": "$APPCONFIG/**" }]
},
{
"identifier": "fs:allow-write",
"allow": [{ "path": "$APPCONFIG/**" }]
}
]
}
tauri.conf.json 윈도우 설정
{
"app": {
"windows": [
{
"label": "main",
"title": "My App",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"center": true,
"decorations": true,
"transparent": false,
"fullscreen": false,
"resizable": true
}
]
}
}
6. 시스템 트레이
// src-tauri/src/tray.rs
use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
menu::{Menu, MenuItem},
Manager, Runtime,
};
pub fn create_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let hide = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?;
let separator = tauri::menu::PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &hide, &separator, &quit])?;
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.tooltip("My App - Running")
.on_menu_event(|app, event| {
match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => {
app.exit(0);
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
// 트레이 아이콘 클릭 시 윈도우 토글
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
let _ = window.show();
let _ = window.set_focus();
}
}
}
})
.build(app)?;
Ok(())
}
트레이 아이콘 동적 변경
use tauri::tray::TrayIcon;
use tauri::image::Image;
fn update_tray_icon(tray: &TrayIcon, status: &str) -> Result<(), Box<dyn std::error::Error>> {
let icon_path = match status {
"active" => "icons/tray-active.png",
"idle" => "icons/tray-idle.png",
"error" => "icons/tray-error.png",
_ => "icons/tray-default.png",
};
let icon = Image::from_path(icon_path)?;
tray.set_icon(Some(icon))?;
tray.set_tooltip(Some(&format!("My App - {}", status)))?;
Ok(())
}
트레이 아이콘 준비물
| OS | 포맷 | 권장 크기 | 비고 |
|---|---|---|---|
| Windows | .ico | 16x16, 32x32 | 시스템 DPI에 따라 선택 |
| macOS | .png (template) | 22x22 @1x, 44x44 @2x | Template 이미지 (흑백) 권장 |
| Linux | .png | 22x22, 24x24 | 데스크탑 환경에 따라 다름 |
7. 메뉴 바
// src-tauri/src/menu.rs
use tauri::{
menu::{Menu, MenuBuilder, MenuItem, PredefinedMenuItem, Submenu, SubmenuBuilder},
Manager,
};
pub fn setup_menu(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let menu = MenuBuilder::new(app)
.items(&[
// File 메뉴
&SubmenuBuilder::new(app, "File")
.items(&[
&MenuItem::with_id(app, "new", "New Session", true, Some("CmdOrCtrl+N"))?,
&MenuItem::with_id(app, "open", "Open...", true, Some("CmdOrCtrl+O"))?,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "settings", "Settings", true, Some("CmdOrCtrl+,"))?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::quit(app, Some("Quit"))?,
])
.build()?,
// Edit 메뉴
&SubmenuBuilder::new(app, "Edit")
.items(&[
&PredefinedMenuItem::undo(app, None)?,
&PredefinedMenuItem::redo(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::cut(app, None)?,
&PredefinedMenuItem::copy(app, None)?,
&PredefinedMenuItem::paste(app, None)?,
&PredefinedMenuItem::select_all(app, None)?,
])
.build()?,
// View 메뉴
&SubmenuBuilder::new(app, "View")
.items(&[
&MenuItem::with_id(app, "zoom_in", "Zoom In", true, Some("CmdOrCtrl+="))?,
&MenuItem::with_id(app, "zoom_out", "Zoom Out", true, Some("CmdOrCtrl+-"))?,
&MenuItem::with_id(app, "zoom_reset", "Actual Size", true, Some("CmdOrCtrl+0"))?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::fullscreen(app, None)?,
])
.build()?,
// Help 메뉴
&SubmenuBuilder::new(app, "Help")
.items(&[
&MenuItem::with_id(app, "docs", "Documentation", true, None::<&str>)?,
&MenuItem::with_id(app, "about", "About", true, None::<&str>)?,
])
.build()?,
])
.build()?;
app.set_menu(menu)?;
// 메뉴 이벤트 처리
app.on_menu_event(|app, event| {
match event.id().as_ref() {
"new" => { /* 새 세션 */ }
"settings" => { /* 설정 윈도우 */ }
"docs" => {
let _ = tauri::api::shell::open(
&app.shell_scope(),
"https://docs.example.com",
None,
);
}
_ => {}
}
});
Ok(())
}
macOS 앱 메뉴 참고사항
| 항목 | 설명 |
|---|---|
| 앱 이름 메뉴 | macOS는 첫 번째 메뉴를 앱 이름으로 자동 표시 |
| 표준 단축키 | Cmd+Q (Quit), Cmd+H (Hide) 등 macOS 표준 준수 |
| Services 메뉴 | macOS에서 자동 추가됨 (PredefinedMenuItem 사용) |
| Edit 메뉴 | 텍스트 입력 시 Undo/Redo/Cut/Copy/Paste 필수 |
8. 프론트엔드 구조
SvelteKit 구조 (권장)
src/
+-- routes/
| +-- +layout.svelte # 루트 레이아웃
| +-- +page.svelte # 메인 페이지
| +-- settings/
| | +-- +page.svelte # 설정 페이지
| +-- session/
| +-- [id]/
| +-- +page.svelte # 세션 상세
+-- lib/
| +-- components/ # UI 컴포넌트
| +-- stores/ # Svelte stores
| +-- api/ # Tauri IPC 래퍼
| | +-- session.ts
| | +-- config.ts
| +-- utils/ # 유틸리티
| +-- types/ # TypeScript 타입
+-- app.html
+-- app.css
React 구조
src/
+-- App.tsx
+-- pages/ # 페이지 컴포넌트
+-- components/ # UI 컴포넌트
+-- features/ # 기능별 모듈
| +-- session/
| | +-- hooks/
| | +-- components/
| | +-- api.ts
| +-- settings/
+-- shared/
| +-- api/ # Tauri IPC 래퍼
| +-- hooks/ # 공용 hooks
| +-- store/ # Zustand/Jotai
| +-- utils/
| +-- types/
Tauri IPC 래퍼 패턴
// src/lib/api/base.ts
import { invoke } from '@tauri-apps/api/core';
export class TauriApiError extends Error {
constructor(public code: string, message: string) {
super(message);
this.name = 'TauriApiError';
}
}
export async function tauriInvoke<T>(
command: string,
args?: Record<string, unknown>,
): Promise<T> {
try {
return await invoke<T>(command, args);
} catch (error) {
if (typeof error === 'string') {
throw new TauriApiError('INVOKE_ERROR', error);
}
throw error;
}
}
Tauri 환경 감지
// src/lib/utils/platform.ts
export function isTauri(): boolean {
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
}
// SvelteKit: SSR 비활성화 (Tauri 앱에 필수)
// svelte.config.js
const config = {
kit: {
adapter: adapterStatic({
fallback: 'index.html',
}),
// SSR 비활성화 (Tauri는 정적 파일 서빙)
prerender: { entries: [] },
},
};
9. Sidecar (외부 바이너리 번들링)
Sidecar는 외부 CLI 바이너리를 앱에 포함시켜 실행하는 기능이다.
사용 사례
| 사례 | 설명 |
|---|---|
| CLI 도구 포함 | Python/Go CLI를 앱에 번들링 |
| FFmpeg | 미디어 변환 |
| AI 런타임 | ONNX Runtime, llama.cpp 등 |
| 데이터베이스 | 임베디드 DB 서버 |
tauri.conf.json 설정
{
"bundle": {
"externalBin": [
"binaries/my-cli"
]
}
}
바이너리 배치 규칙
src-tauri/binaries/
+-- my-cli-x86_64-pc-windows-msvc.exe # Windows x64
+-- my-cli-aarch64-apple-darwin # macOS ARM
+-- my-cli-x86_64-apple-darwin # macOS Intel
+-- my-cli-x86_64-unknown-linux-gnu # Linux x64
파일명은 반드시
{name}-{target_triple}[.exe]형식을 따라야 한다. target triple은rustc -Vv | grep host로 확인.
Sidecar 실행
// Rust에서 sidecar 실행
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn run_cli(app: tauri::AppHandle, args: Vec<String>) -> Result<String, String> {
let output = app
.shell()
.sidecar("my-cli")
.map_err(|e| e.to_string())?
.args(&args)
.output()
.await
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
// Frontend에서 sidecar 실행
import { Command } from '@tauri-apps/plugin-shell';
const command = Command.sidecar('my-cli', ['--format', 'json']);
const output = await command.execute();
if (output.code === 0) {
const result = JSON.parse(output.stdout);
console.log(result);
} else {
console.error(output.stderr);
}
Sidecar Capability 설정
// capabilities/default.json
{
"permissions": [
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "my-cli",
"cmd": "",
"args": true,
"sidecar": true
}
]
}
]
}
Sidecar 체크리스트
- 모든 타겟 플랫폼용 바이너리 준비 (target triple 확인)
-
externalBin경로에 바이너리 배치 - Capability에 sidecar 실행 권한 추가
- CI/CD에서 플랫폼별 바이너리 빌드/다운로드 자동화
- 바이너리 실행 권한 (chmod +x) 확인 (Linux/macOS)
- macOS: 코드 서명 + notarization 시 sidecar도 서명