Claude Code로 Tauri 데스크톱 앱 개발하기: Vite, Rust 명령, 권한 설계
Claude Code로 Tauri v2 앱을 만들며 Vite, Rust command, invoke, 파일 권한, 테스트와 리뷰 프롬프트를 정리합니다.
Tauri는 Web UI를 Windows, macOS, Linux 데스크톱 앱으로 배포하게 해주는 프레임워크입니다. 화면은 React나 Svelte 같은 프론트엔드로 만들고, 로컬 파일 접근이나 OS와 가까운 처리는 Rust 쪽에 둡니다. 이 구조는 Claude Code와 잘 맞습니다. UI, Rust command, capability, build/test를 작은 단위로 나누어 맡길 수 있기 때문입니다.
이 글은 Tauri v2에서 로컬 메모 앱을 만든다는 가정으로 진행합니다. 언제 Tauri를 선택해야 하는지, Vite + React 또는 Svelte로 시작하는 방법, Rust command 작성, 프론트엔드 invoke, 파일 시스템 권한, 테스트와 빌드, Claude Code에 권한 리뷰를 시키는 프롬프트까지 다룹니다.
공식 자료는 Tauri Create a Project, Calling Rust from the Frontend, Tauri Capabilities, File System plugin, Vite Getting Started, Cargo test, Claude Code setup을 기준으로 확인했습니다. Rust 기초는 Claude Code Rust 개발을, Electron과 비교하려면 Electron 데스크톱 앱 개발을 함께 보세요.
Tauri를 선택할 때
Tauri는 로컬 기능이 필요한 앱에 잘 맞습니다. 개인 메모, 사내 CSV 변환기, 개발자 로그 뷰어, 오프라인 현장 입력 도구처럼 파일을 읽거나 쓰고, 네트워크가 없어도 동작해야 하며, 웹 UI의 개발 속도도 필요한 경우입니다.
반대로 로그인 후 서버 API만 호출하고 로컬 파일이나 OS 기능이 거의 없다면 일반 웹 앱이나 PWA가 더 단순합니다. Tauri를 고르면 Rust 툴체인, 번들링, 서명, 자동 업데이트, 앱 식별자, OS별 동작 차이까지 책임져야 합니다. “가볍다”보다 “로컬 권한을 안전하게 다뤄야 한다”가 선택 기준이 되어야 합니다.
첫 Claude Code 프롬프트는 이렇게 시작하는 편이 안전합니다.
Tauri v2 로컬 메모 앱을 만듭니다.
아직 구현하지 말고 React UI, Rust command, capability, build/test 네 계층으로 작업 계획을 나누세요.
파일 접근은 app data 디렉터리 안으로만 제한하고, 임의 경로 읽기/쓰기는 제안하지 마세요.
이렇게 하면 Claude Code가 처음부터 넓은 파일 권한을 추가하는 상황을 줄일 수 있습니다.
구조와 보안 경계
Tauri 앱에서 프론트엔드는 운영체제를 직접 만지지 않습니다. @tauri-apps/api/core의 invoke로 Rust command를 호출하고, Rust가 입력을 검증한 뒤 로컬 리소스를 다룹니다.
flowchart LR
UI["React 또는 Svelte UI"] --> Invoke["invoke from @tauri-apps/api/core"]
Invoke --> Command["Rust command"]
Command --> Guard["경로 허용 목록과 검증"]
Guard --> AppData["app data directory"]
Command --> Result["typed result"]
Result --> UI
Capability["Tauri capability"] --> UI
중요한 점은 capability가 모든 것을 자동으로 지켜주지는 않는다는 것입니다. capability는 프론트엔드가 호출할 수 있는 Tauri API와 플러그인 권한을 제한합니다. 하지만 직접 작성한 Rust command가 임의 경로를 받아 파일을 쓴다면 그 command 자체가 위험합니다. 그래서 capability와 Rust의 경로 검증을 함께 리뷰해야 합니다.
React 또는 Svelte로 시작하기
새 프로젝트라면 공식 create-tauri-app이 가장 깔끔합니다. 템플릿은 React 또는 Svelte, 언어는 TypeScript를 선택합니다.
npm create tauri-app@latest taskdesk
cd taskdesk
npm install
npm run tauri dev
이미 Vite 앱이 있거나 프론트엔드를 먼저 만들고 싶다면 다음 순서가 현실적입니다. Vite 공식 문서는 npm create vite@latest를 안내하며, 현재 Vite는 Node.js 20.19+ 또는 22.12+가 필요합니다.
node --version
npm create vite@latest taskdesk -- --template react-ts
cd taskdesk
npm install
npm install -D @tauri-apps/cli@latest
npm install @tauri-apps/api@latest
npx tauri init
npx tauri dev
Svelte라면 템플릿만 바꿉니다.
npm create vite@latest taskdesk -- --template svelte-ts
생성 직후에는 Claude Code에 파일을 읽게만 합니다.
package.json, tauri.conf.json, src-tauri/src/lib.rs를 읽고 현재 구조와 스크립트를 요약하세요.
아직 파일은 수정하지 마세요.
템플릿은 계속 바뀌므로, 최신 생성 결과를 먼저 이해시키는 것이 오래된 예시를 억지로 적용하는 것보다 안전합니다.
최소 Tauri 설정
tauri.conf.json은 Vite 개발 서버, 프로덕션 빌드 결과, 윈도우, capability를 연결합니다.
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "TaskDesk",
"version": "0.1.0",
"identifier": "com.example.taskdesk",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:5173",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "TaskDesk",
"width": 1000,
"height": 700
}
],
"security": {
"capabilities": ["main-capability"]
}
},
"bundle": {
"active": true,
"targets": "all"
}
}
리뷰 포인트는 세 가지입니다. identifier를 예시값 그대로 두지 않았는지, devUrl이 Vite 포트와 맞는지, frontendDist가 실제 빌드 출력인 dist를 가리키는지 확인합니다.
Rust command 작성
다음 예시는 메모 읽기를 담당하는 command입니다. 핵심은 파일을 읽는 코드가 아니라 안전하지 않은 상대 경로, 절대 경로, .. 접근을 거부하는 부분입니다.
// src-tauri/src/note_commands.rs
use serde::Serialize;
use std::{
fs,
path::{Component, Path, PathBuf},
};
use tauri::{AppHandle, Manager};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoteFile {
name: String,
path: String,
bytes: u64,
is_dir: bool,
}
fn reject_unsafe_relative(path: &Path) -> Result<(), String> {
for component in path.components() {
match component {
Component::Normal(_) | Component::CurDir => {}
_ => return Err("use a relative path inside app data".to_string()),
}
}
Ok(())
}
fn app_data_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("failed to get app data dir: {error}"))?;
fs::create_dir_all(&root).map_err(|error| format!("failed to create app data dir: {error}"))?;
root.canonicalize()
.map_err(|error| format!("failed to resolve app data dir: {error}"))
}
fn existing_path(app: &AppHandle, relative: &str) -> Result<PathBuf, String> {
let root = app_data_root(app)?;
let requested = Path::new(relative);
reject_unsafe_relative(requested)?;
let full = root
.join(requested)
.canonicalize()
.map_err(|error| format!("path does not exist: {error}"))?;
if !full.starts_with(&root) {
return Err("path escapes app data".to_string());
}
Ok(full)
}
#[tauri::command]
pub fn read_note(app: AppHandle, path: String) -> Result<String, String> {
let safe_path = existing_path(&app, &path)?;
fs::read_to_string(safe_path).map_err(|error| format!("failed to read note: {error}"))
}
별도 모듈에 command를 두었다면 lib.rs에서 등록합니다.
// src-tauri/src/lib.rs
mod note_commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
note_commands::read_note
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
프론트엔드 invoke 래퍼
컴포넌트마다 command 이름을 반복하지 말고 작은 API 래퍼를 둡니다.
// src/lib/notesApi.ts
import { invoke } from "@tauri-apps/api/core";
export type NoteFile = {
name: string;
path: string;
bytes: number;
isDir: boolean;
};
export const notesApi = {
read(path: string) {
return invoke<string>("read_note", { path });
},
write(path: string, content: string) {
return invoke<void>("write_note", { path, content });
},
list(dir = ".") {
return invoke<NoteFile[]>("list_notes", { dir });
},
};
작은 React 화면은 다음처럼 시작할 수 있습니다.
import { useState } from "react";
import { notesApi } from "./lib/notesApi";
export default function App() {
const [content, setContent] = useState("");
const [message, setMessage] = useState("Ready");
async function saveNote() {
await notesApi.write("daily-note.txt", content);
setMessage("Saved");
}
return (
<main>
<textarea value={content} onChange={(event) => setContent(event.target.value)} />
<button onClick={saveNote}>Save</button>
<p>{message}</p>
</main>
);
}
파일 권한과 capability
프론트엔드가 File System plugin을 직접 호출한다면 capability를 명확히 작성합니다.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Main window permissions for TaskDesk.",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-app-read-recursive",
"fs:allow-app-write-recursive"
]
}
이 글의 예시는 Rust command가 파일을 다루므로, 프론트엔드에 다운로드 폴더나 문서 폴더 전체 권한을 줄 필요가 없습니다. 권한을 변경했다면 Claude Code에 리뷰만 시킵니다.
리뷰만 하고 수정하지 마세요.
src-tauri/capabilities와 src-tauri/src/note_commands.rs를 검사하세요.
프론트엔드에서 호출 가능한 API와 Rust command를 모두 나열하세요.
각 API가 접근 가능한 파일 경로 범위를 설명하세요.
절대 경로, .., 넓은 wildcard, app data 밖 쓰기를 지적하세요.
기능을 유지하는 최소 권한을 제안하세요.
빌드, 테스트, 활용 사례
확인은 세 단계로 나눕니다.
npm run build
cd src-tauri
cargo test
cd ..
npm run tauri build
npm run build는 Vite, cargo test는 Rust, npm run tauri build는 데스크톱 패키징을 확인합니다. 경로 검증은 작은 단위 테스트로 먼저 잠급니다.
#[cfg(test)]
mod tests {
use super::reject_unsafe_relative;
use std::path::Path;
#[test]
fn rejects_parent_directory() {
assert!(reject_unsafe_relative(Path::new("../secret.txt")).is_err());
}
}
실제 유스케이스는 네 가지가 좋습니다. 첫째, 로컬 메모와 일지 앱. 둘째, CSV나 Markdown 변환기. 셋째, 개발자 로그 뷰어. 넷째, 네트워크가 불안정한 현장 입력 앱입니다. 공통점은 로컬 데이터와 명확한 권한 경계가 필요하다는 점입니다.
흔한 함정은 capability를 Rust command 보안으로 착각하는 것, Claude Code에 한 번에 전체 앱을 맡기는 것, 개발 설정을 릴리스까지 가져가는 것, 한 OS에서만 테스트하는 것, 편의를 위해 프론트엔드 파일 권한을 넓게 여는 것입니다.
수익 CTA와 확인 결과
이 흐름을 팀 표준으로 만들고 싶다면 ClaudeCodeLab products에서 템플릿과 설정 가이드를 먼저 확인하세요. 실제 저장소를 기준으로 권한, CLAUDE.md, 리뷰 프롬프트, 배포 전 확인을 정리하려면 Claude Code training and consultation이 더 맞습니다.
이 글의 흐름을 작은 메모 앱 기준으로 점검했을 때 가장 안정적인 순서는 Rust 경로 경계를 먼저 만들고, TypeScript invoke 래퍼를 맞춘 뒤, 마지막에 capability를 리뷰하는 방식이었습니다. UI부터 만들면 빨라 보이지만 저장 위치와 권한이 뒤늦게 바뀌면서 수정 범위가 커졌습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.