본문으로 건너뛰기

Tauri v2 아키텍처 가이드

작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Tauri v2 데스크탑 앱 (Windows / macOS / Linux) 공통 참조: ../common/ (프로젝트 구조, 보안, 인증, 테스트 등)


목차

  1. Tauri v2 아키텍처 개요
  2. IPC 통신
  3. Capabilities & Permissions
  4. Plugin 시스템
  5. Multi-Window / Multi-WebView
  6. 시스템 트레이
  7. 메뉴 바
  8. 프론트엔드 구조
  9. 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 주요 차이

항목v1v2
보안 모델allowlist (boolean 토글)Capabilities + Permissions + Scopes
모바일 지원없음iOS / Android 지원
플러그인 시스템기본ACL 기반 권한 내장
IPCinvoke + listeninvoke + 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 메커니즘 비교

메커니즘방향용도패턴
invokeFrontend -> Rust명령 실행 + 결과 반환Request-Response
events양방향비동기 알림, 브로드캐스트Pub-Sub
channelsRust -> 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

권한 설정 원칙

원칙설명
최소 권한필요한 기능만 허용 (denyallow보다 우선)
윈도우 분리각 윈도우/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-storeKey-Value 영구 저장소
tauri-plugin-fs@tauri-apps/plugin-fs파일시스템 접근
tauri-plugin-http@tauri-apps/plugin-httpHTTP 클라이언트
tauri-plugin-notification@tauri-apps/plugin-notificationOS 알림
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-osOS 정보
tauri-plugin-log@tauri-apps/plugin-log로깅
tauri-plugin-sql@tauri-apps/plugin-sqlSQLite/MySQL/PostgreSQL
tauri-plugin-global-shortcut@tauri-apps/plugin-global-shortcut글로벌 단축키
tauri-plugin-deep-link@tauri-apps/plugin-deep-linkDeep Link / Custom Protocol
tauri-plugin-autostart@tauri-apps/plugin-autostartOS 시작 시 자동 실행
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.ico16x16, 32x32시스템 DPI에 따라 선택
macOS.png (template)22x22 @1x, 44x44 @2xTemplate 이미지 (흑백) 권장
Linux.png22x22, 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도 서명

아키텍처 설계 체크리스트

초기 프로젝트 셋업

  • tauri init 으로 프로젝트 생성
  • tauri.conf.json 기본 설정 (앱 이름, identifier, 버전)
  • capabilities/default.json 최소 권한 설정
  • 필요 플러그인 선정 및 등록
  • 프론트엔드 프레임워크 선택 (SvelteKit / React)
  • IPC 커맨드 구조 설계 (commands/ 폴더)
  • 앱 상태 관리 방식 결정 (Rust State + Frontend Store)

보안

  • Capabilities에 최소 권한만 부여
  • HTTP 요청 허용 URL 명시적 제한
  • 파일시스템 접근 범위 $APPDATA 등으로 제한
  • 쉘 명령 실행은 사전 정의된 것만 허용
  • CSP 설정 완료 (tauri.conf.json > app.security.csp)
  • IPC 커맨드 입력값 검증 (Rust 측)

배포 준비

  • 앱 아이콘 준비 (모든 OS 크기)
  • 시스템 트레이 아이콘 준비
  • 메뉴 바 구성 (macOS 표준 준수)
  • Multi-Window 권한 분리 (필요 시)
  • Sidecar 바이너리 준비 (필요 시)