Tauri v2로 데스크톱 앱 만들기 — Electron 대신 선택한 이유와 실사용기
AI 프롬프트 엔지니어링 도구 Promptory를 만들면서 겪은 Tauri v2 실전 경험을 공유합니다.React 19 + Vite 7 + SQLite 조합으로 크로스 플랫폼 데스크톱 앱을 만든 이야기.
Author
Younggun Park
Frontend engineer and builder · Seoul, South Korea
I'm building Vision AI products at P2ACH AI, writing about frontend systems, AI tooling, and the quiet parts of shipping.
TL;DR
AI 프롬프트 엔지니어링 도구 Promptory를 만들면서 겪은 Tauri v2 실전 경험을 공유합니다.React 19 + Vite 7 + SQLite 조합으로 크로스 플랫폼 데스크톱 앱을 만든 이야기.
On this page
AI 프롬프트 엔지니어링 도구 Promptory를 만들면서 겪은 Tauri v2 실전 경험을 공유합니다.
React 19 + Vite 7 + SQLite 조합으로 크로스 플랫폼 데스크톱 앱을 만든 이야기.
왜 Electron이 아니라 Tauri인가
데스크톱 앱을 만들겠다고 마음먹으면 제일 먼저 떠오르는 건 Electron이죠. VS Code, Slack, Discord… 검증된 선택지입니다. 그런데 저는 Tauri v2를 골랐습니다.
번들 크기가 다릅니다
Electron은 Chromium 전체를 앱에 포함시킵니다. 빈 프로젝트도 150MB가 넘어요. Tauri는 OS 네이티브 WebView를 쓰기 때문에 앱 바이너리가 10MB 내외입니다. Promptory의 macOS .dmg 파일이 약 12MB — 사용자한테 "이거 좀 무거운 앱이에요"라고 변명할 필요가 없습니다.
메모리를 적게 먹습니다
Electron 앱 하나 띄우면 최소 200~300MB의 RAM을 차지합니다. Tauri 앱은 50~80MB 수준입니다. 개발자라면 이미 브라우저 탭 50개, Docker, IDE를 돌리고 있을 텐데, 데스크톱 앱 하나 더 띄웠다고 팬이 돌아가면 안 되잖아요.
Rust 백엔드의 안정감
Electron의 Node.js 백엔드 대신 Rust를 쓴다는 게 처음엔 부담이었습니다. "Rust 모르는데 괜찮을까?" 싶었죠. 결론부터 말하면 — Rust를 거의 안 써도 됩니다. 이건 뒤에서 자세히 얘기할게요.
Promptory: 이걸로 뭘 만들었나
Promptory는 AI 프롬프트를 블록 단위로 조립하는 로컬 데스크톱 앱입니다.
- 블록 라이브러리: Persona, Constraint, Instruction 같은 타입별 프롬프트 블록을 관리
- 캔버스 에디터: 블록을 드래그 앤 드롭으로 조합해서 프롬프트를 구성
- 실시간 토큰 카운트: tiktoken WASM으로 GPT-4o, Claude 모델별 토큰 수를 실시간 표시
- 다중 포맷 내보내기: 일반 텍스트, JSON, Cursor Rules, CLAUDE.md, OpenAI API 형식 등
- 버전 스냅샷: 프롬프트의 특정 시점을 저장하고 비교
클라우드 없이, 내 컴퓨터 안의 SQLite 하나에 모든 데이터가 들어갑니다. "로컬 퍼스트"를 지향했습니다.
기술 스택 한눈에 보기
최신 스택을 욕심 좀 부려서 골랐는데, Tauri v2가 이 조합을 아무 마찰 없이 받아들여 줬습니다. Vite 7이든 React 19든 상관없이 — Tauri 입장에서는 결국 localhost:1420에서 뜨는 웹앱을 WebView에 띄우는 거니까요.
놀라운 점: Rust를 거의 안 썼습니다
Tauri 하면 "아, Rust 해야 되는 거 아니야?"라는 반응이 먼저 옵니다. 저도 그랬습니다. 그런데 실제로 Promptory의 Rust 코드는 딱 50줄 정도입니다.
메인 진입점 — lib.rs
Rust
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:promptory.db", db::migrations())
.build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.invoke_handler(tauri::generate_handler![commands::export::export_to_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}플러그인 등록하고, 커맨드 하나 노출하고, 끝입니다.
유일한 Rust 커맨드 — 파일 내보내기
Rust
#[tauri::command]
pub async fn export_to_file(path: String, contents: String) -> Result<(), String> {
std::fs::write(&path, &contents).map_err(|e| e.to_string())
}이게 전부입니다. 진짜로요.
왜 이게 가능한가?
Tauri v2의 플러그인 생태계 덕분입니다. tauri-plugin-sql이 JavaScript에서 직접 SQLite를 조작할 수 있는 API를 제공합니다:
TypeScript
import Database from '@tauri-apps/plugin-sql';
const db = await Database.load('sqlite:promptory.db');
await db.execute(
'INSERT INTO blocks (id, type, title, content, tags, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, type, title, content, JSON.stringify(tags), now, now]
);
const blocks = await db.select<Block[]>('SELECT * FROM blocks ORDER BY updated_at DESC');CRUD 전체를 JavaScript로 합니다. Rust ORM 같은 건 필요 없었어요. 파일 시스템, 클립보드, 앱 업데이터까지 전부 플러그인으로 해결됩니다.
결론: Rust를 모르더라도 Tauri v2로 앱을 만들 수 있습니다. 물론 Rust를 알면 더 강력한 네이티브 기능을 구현할 수 있지만, "몰라서 못 만든다"는 건 이제 옛말입니다.
DB 설계: 로컬 SQLite의 실용성
스키마가 단순합니다
Plain
blocks — 재사용 가능한 프롬프트 블록
prompts — 프롬프트 메타데이터
prompt_blocks — 프롬프트 ↔ 블록 관계 (순서, 오버라이드 포함)
prompt_snapshots — 버전 스냅샷네 개 테이블이 전부입니다. 프롬프트 조립 도구에 이 이상의 복잡한 스키마는 필요 없었습니다.
마이그레이션은 Rust에서 관리합니다
Rust
Migration {
version: 1,
description: "create core tables",
sql: "CREATE TABLE blocks (...); CREATE TABLE prompts (...); ...",
kind: MigrationKind::Up,
}앱이 뜰 때 자동으로 마이그레이션이 실행됩니다. 사용자는 아무것도 안 해도 됩니다.
브라우저 폴백: 테스트를 위한 결정
재밌는 설계 포인트가 하나 있습니다. Tauri 없이도 앱이 돌아가게 만들었습니다:
TypeScript
// db.ts
if (isTauriRuntime()) {
return await Database.load(DB_PATH);
} else {
// localStorage 기반 폴백
return createLocalStorageDb();
}왜 이런 걸 했느냐면 — 테스트 때문입니다. Vitest는 브라우저 환경(jsdom)에서 돌아가는데, Tauri 런타임이 없으니까 SQLite 플러그인을 쓸 수가 없습니다. localStorage 폴백을 만들어 놓으니 서비스 레이어 단위 테스트가 Tauri 없이도 잘 돌아갑니다.
이 패턴, Tauri 앱 테스트할 때 꽤 유용합니다. 추천합니다.
프론트엔드 아키텍처: 웹 개발하듯이 합니다
Zustand이 딱 맞았습니다
상태 관리는 Zustand v5를 썼습니다. 4개의 독립 스토어:
- PromptStore: 활성 프롬프트, 캔버스 블록, undo/redo
- LibraryStore: 블록 라이브러리, 검색/필터
- UIStore: 사이드바 탭, 테마, 패널 표시 여부
- TokenStore: 모델별 토큰 카운트
Redux였으면 보일러플레이트가 배로 늘었을 겁니다. Zustand은 스토어 하나가 함수 하나입니다. Tauri와의 궁합도 좋습니다 — IPC 호출 결과를 스토어에 넣는 것도, 일반 fetch 결과 넣는 것과 다를 게 없거든요.
드래그 앤 드롭: @dnd-kit/react의 새 API
블록을 라이브러리에서 캔버스로 드래그하거나, 캔버스 안에서 순서를 바꾸는 게 핵심 인터랙션입니다. @dnd-kit/react 0.3의 새로운 API를 썼는데, 핵심 패턴은 이겁니다:
TypeScript
// AppShell.tsx에서 단일 DragDropProvider
<DragDropProvider onDragEnd={handleDragEnd}>
<Sidebar /> {/* 라이브러리 블록: origin = "library" */}
<Canvas /> {/* 캔버스 블록: origin = "canvas" */}
</DragDropProvider>드래그 데이터에 origin 필드를 넣어서, 드롭 핸들러에서 분기합니다:
library→ 캔버스: 새 블록 추가canvas→ 캔버스: 순서 변경
Provider 하나로 두 가지 드래그 시나리오를 처리하는 깔끔한 패턴입니다.
CodeMirror 6: 에디터는 이걸로
마크다운 에디터로 CodeMirror 6을 썼습니다. 한 가지 중요한 패턴이 있는데 — 비제어 컴포넌트(uncontrolled) 방식으로 씁니다:
TypeScript
// 마운트 시 한 번만 생성, React state와 분리
const view = new EditorView({
state: EditorState.create({ doc: initialContent, extensions }),
parent: containerRef.current,
});React state로 에디터 내용을 관리하면 커서가 튀는 문제가 생깁니다. 에디터는 자기 state를 자기가 관리하고, 외부에서 변경이 필요할 때만 dispatch()로 넣어주는 방식이 안정적이었습니다.
빌드와 배포: 크로스 플랫폼 자동화
Vite 설정은 웹 프로젝트와 동일합니다
TypeScript
// vite.config.ts
export default defineConfig({
plugins: [
react(),
tailwindcss(),
wasm(), // tiktoken WASM 지원
topLevelAwait(), // 비동기 초기화
],
server: {
port: 1420,
strictPort: true,
},
});WASM 플러그인을 쓴 이유는 tiktoken 때문입니다. OpenAI의 토크나이저가 WASM으로 제공되는데, Vite에서 이걸 깔끔하게 로드하려면 vite-plugin-wasm이 필요합니다.
코드 스플리팅으로 초기 로딩 최적화
TypeScript
manualChunks: {
'tokenizer': ['tiktoken'],
'codemirror-core': ['@codemirror/view', '@codemirror/state'],
'codemirror-markdown': ['@codemirror/lang-markdown'],
'dnd-kit': ['@dnd-kit/react', '@dnd-kit/helpers'],
'react-vendor': ['react', 'react-dom'],
'state-vendor': ['zustand'],
}데스크톱 앱이지만 코드 스플리팅은 여전히 의미 있습니다. WebView의 초기 로딩이 빨라지니까요. 특히 tiktoken WASM은 무거우니 별도 청크로 분리해서 필요할 때만 로드합니다.
GitHub Actions로 4개 플랫폼 자동 빌드
YAML
strategy:
matrix:
include:
- platform: macos-latest # Apple Silicon
args: --target aarch64-apple-darwin
- platform: macos-latest # Intel Mac
args: --target x86_64-apple-darwin
- platform: ubuntu-22.04 # Linux
- platform: windows-latest # WindowsGit 태그(v*)를 푸시하면 4개 플랫폼 빌드가 동시에 돌아가고, GitHub Releases에 바이너리가 올라갑니다. Tauri의 tauri-apps/tauri-action이 코드 서명까지 처리해 줍니다.
자동 업데이트
JSON
// tauri.conf.json
"updater": {
"pubkey": "...",
"endpoints": [
"<https://github.com/jadru/promptory/releases/latest/download/latest.json>"
]
}앱이 시작할 때 GitHub Releases의 latest.json을 확인하고, 새 버전이 있으면 사용자에게 알려줍니다. Electron의 electron-updater와 비슷한 경험인데, 설정이 더 간단합니다.
Tauri v2에서 삽질한 것들
좋은 얘기만 하면 블로그 글이 아니죠. 실제로 겪은 문제들입니다.
1. CSP 설정의 함정
Tauri v2는 기본적으로 엄격한 CSP(Content Security Policy)를 적용합니다. WASM 로딩, 인라인 스타일, eval() 같은 게 차단돼요. 처음에 tiktoken WASM이 안 되는 바람에 한참 헤맸습니다.
결국 개발 단계에서는 CSP를 null로 풀어 놓았습니다:
JSON
"security": {
"csp": null
}프로덕션에서는 필요한 디렉티브만 열어주는 게 맞지만, 초기 개발 속도를 위해 이렇게 시작하는 것도 방법입니다.
2. 테스트 환경에서의 Tauri 부재
Vitest는 Node.js/jsdom 환경에서 돌아갑니다. @tauri-apps/plugin-sql은 Tauri 런타임이 있어야 동작합니다. 이 갭을 메우는 게 생각보다 귀찮았습니다.
해결책은 위에서 말한 localStorage 폴백인데, 이걸 처음부터 설계에 넣어야 했습니다. 나중에 붙이려니 기존 코드를 꽤 고쳐야 했어요.
교훈: Tauri 앱을 만들 때 "Tauri 없이도 돌아가는 서비스 레이어"를 초기부터 만들어 두세요.
3. macOS 코드 서명
이건 Tauri의 문제라기보다 Apple의 문제인데… macOS 배포를 위한 코드 서명과 공증(notarization)은 여전히 복잡합니다. Apple Developer 계정, 인증서, 프로비저닝 프로필… Tauri의 GitHub Action이 많은 부분을 자동화해 주지만, 처음 설정할 때 삽질은 각오하세요.
4. 리눅스 의존성
Ubuntu에서 빌드하려면 WebKitGTK 관련 시스템 패키지가 필요합니다:
Bash
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-devCI에서 이거 안 깔아놓으면 빌드가 실패합니다. GitHub Actions 워크플로우에 꼭 넣어 두세요.
Electron과의 솔직한 비교
솔직히 Electron이 나은 경우
- WebView 일관성이 중요할 때: Tauri는 macOS에서 WebKit, Windows에서 WebView2(Edge), Linux에서 WebKitGTK를 씁니다. 브라우저별 CSS/JS 차이가 있을 수 있습니다. Electron은 Chromium 하나니까 이런 걱정이 없어요.
- Node.js 생태계에 깊이 의존할 때: 파일 스트리밍, 네이티브 모듈 (
sharp,better-sqlite3등)을 직접 써야 한다면 Electron이 편합니다. - 이미 Electron 경험이 풍부한 팀: 굳이 바꿀 이유가 없을 수 있습니다.
Tauri가 나은 경우
- 앱 크기와 성능이 중요할 때 (Promptory 같은 유틸리티 앱)
- 간단한 네이티브 기능만 필요할 때 (파일 I/O, DB, 클립보드 수준)
- Rust의 안전성과 성능이 필요한 백엔드 로직이 있을 때
- "가벼운 앱"이라는 사용자 인상을 주고 싶을 때
마치며: 결국 "좋은 도구"입니다
Tauri v2는 제가 기대한 것 이상이었습니다. 특히 플러그인 시스템의 성숙도가 인상적이었어요. SQLite, 파일 시스템, 클립보드, 자동 업데이트 — 데스크톱 앱에서 보통 필요한 것들이 전부 플러그인으로 있고, JavaScript API가 잘 설계되어 있습니다.
Rust를 몰라도 됩니다. 물론 알면 더 좋지만, "Rust 때문에 Tauri 못 쓰겠다"는 건 이제 핑계에 가깝습니다.
웹 개발 경험이 있고, 번들 크기와 성능에 신경을 쓰는 분이라면 — Tauri v2를 한번 써보세요. 생각보다 많이 편하고, 결과물도 깔끔합니다.
FAQ
Common follow-up questions
- 이 글의 핵심은 무엇인가요?
- AI 프롬프트 엔지니어링 도구 Promptory를 만들면서 겪은 Tauri v2 실전 경험을 공유합니다.React 19 + Vite 7 + SQLite 조합으로 크로스 플랫폼 데스크톱 앱을 만든 이야기.
- 실무적으로 먼저 볼 포인트는 무엇인가요?
- 데스크톱 앱을 만들겠다고 마음먹으면 제일 먼저 떠오르는 건 Electron이죠. VS Code, Slack, Discord… 검증된 선택지입니다. 그런데 저는 Tauri v2를 골랐습니다.
Tags
Related posts
View all writing코틀린은 자바와 비슷하지만 다른 언어이다. Jetbrains의 세심한 배려가 있는 코틀린의 매력은 코틀린 시리즈에서 차차 설명해볼까 한다.
Dec 30, 2021