Tauri로 가벼운 데스크톱 앱 만들기
웹 개발자라면 한 번쯤 “내가 만든 웹 앱을 데스크톱 앱으로 배포할 수 있으면 좋겠다”고 생각해 보신 적 있을 겁니다. Electron이 이 영역을 오랫동안 지배해 왔지만, 앱 하나에 Chromium 전체를 번들하다 보니 설치 파일이 100MB를 넘어가는 건 일상이었죠 😅
Tauri는 이 문제를 풀려고 만들어진 프레임워크입니다. 프론트엔드는 기존 웹 기술을 그대로 쓰되 백엔드를 Rust로 작성하고, OS의 네이티브 웹뷰를 활용해서 앱 크기를 수 MB 수준으로 줄여 줘요.
이번 글에서는 Tauri 2로 데스크톱 앱을 처음부터 만들어 보면서, 프론트엔드와 Rust 백엔드를 연동하는 핵심 패턴을 살펴보겠습니다.
Tauri란?
Tauri는 웹 기술(HTML, CSS, JavaScript)로 UI를 만들고 Rust로 시스템 기능을 처리하는 데스크톱 앱 프레임워크입니다. Electron과 가장 큰 차이는 브라우저 엔진을 앱에 포함시키지 않는다는 점이에요. 각 OS에 이미 설치되어 있는 웹뷰(macOS의 WebKit, Windows의 WebView2, Linux의 WebKitGTK)를 그대로 가져다 씁니다.
그래서 빌드된 앱 크기가 확 줄어듭니다. Electron 앱이 보통 150MB 이상인 반면 Tauri 앱은 같은 기능을 10MB 이하로 만들 수 있어요. 메모리 사용량도 훨씬 적고 시작 속도도 빠르죠.
Tauri 2부터는 iOS와 Android까지 지원하기 시작했습니다. 하나의 코드베이스로 데스크톱과 모바일을 모두 커버할 수 있게 된 거죠.
개발 환경 준비
Tauri 개발을 시작하려면 Rust 툴체인과 Node.js(또는 Bun)가 필요합니다.
Rust는 공식 설치 도구인 rustup으로 설치합니다.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
설치가 끝나면 rustc와 cargo 명령이 사용 가능한지 확인합니다.
rustc --version
cargo --version
OS별로 추가 의존성도 필요합니다. macOS에서는 Xcode Command Line Tools가 필요하고, Linux에서는 WebKitGTK와 관련 라이브러리를 설치해야 해요. Windows는 WebView2가 필요하지만 Windows 10 이상에서는 대부분 기본으로 깔려 있으니 크게 신경 쓸 건 없습니다.
macOS라면 다음 명령으로 충분합니다.
xcode-select --install
Linux(Ubuntu/Debian 기준)에서는 필수 패키지를 설치해야 합니다.
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
프로젝트 생성
Tauri 프로젝트를 가장 빠르게 시작하려면 create-tauri-app 스캐폴딩 도구를 사용하면 됩니다.
bunx create-tauri-app
실행하면 프로젝트 이름, 프론트엔드 프레임워크(React, Vue, Svelte, Vanilla 등), 언어(TypeScript, JavaScript) 등을 선택하는 대화형 프롬프트가 나옵니다. 여기서는 React + TypeScript 조합으로 진행해 보겠습니다.
✔ Project name · my-tauri-app
✔ Identifier · com.my-tauri-app.app
✔ Choose which language to use for your frontend · TypeScript / JavaScript
✔ Choose your package manager · bun
✔ Choose your UI template · React
✔ Choose your UI flavor · TypeScript
생성된 프로젝트 디렉토리로 이동해서 의존성을 설치합니다.
cd my-tauri-app
bun install
프로젝트 구조
생성된 프로젝트를 열어보면 웹 프로젝트와 Rust 프로젝트가 하나로 합쳐진 구조입니다.
my-tauri-app/
├── src/ # 프론트엔드 (React + TypeScript)
│ ├── App.tsx
│ ├── App.css
│ └── main.tsx
├── src-tauri/ # Rust 백엔드
│ ├── src/
│ │ └── lib.rs # Rust 커맨드 정의
│ ├── Cargo.toml # Rust 의존성
│ ├── tauri.conf.json # Tauri 설정
│ └── capabilities/ # 권한 설정
├── index.html
├── package.json
└── vite.config.ts
src/ 디렉토리는 일반적인 Vite + React 프로젝트와 동일합니다.
핵심은 src-tauri/ 디렉토리인데, 여기에 Rust 백엔드 코드와 Tauri 설정이 들어갑니다.
개발 서버 실행
개발 서버를 실행하면 프론트엔드 핫 리로드와 함께 네이티브 윈도우가 열립니다.
bun run tauri dev
처음 실행할 때는 Rust 코드를 컴파일해야 해서 시간이 좀 걸려요. 하지만 이후에는 변경된 부분만 다시 컴파일하니까 금방입니다. 프론트엔드 코드를 수정하면 브라우저처럼 즉시 반영되고, Rust 코드를 수정하면 자동으로 다시 컴파일됩니다.
Rust 커맨드 작성
Tauri가 흥미로운 건 Rust로 시스템 수준의 기능을 구현하고 프론트엔드에서 바로 호출할 수 있다는 점이에요.
src-tauri/src/lib.rs 파일을 열어보면 기본 커맨드가 이미 만들어져 있습니다.
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command] 매크로를 붙이면 해당 함수를 프론트엔드에서 호출할 수 있게 됩니다.
프론트엔드에서 넘긴 값은 Rust가 자동으로 역직렬화해 주고, 반환값도 직렬화해서 프론트엔드로 보내 줍니다.
새로운 커맨드를 추가해 봅시다. 파일 시스템에 접근하는 간단한 예제를 만들어 보겠습니다.
use std::fs;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Result<String, String>을 반환하면 프론트엔드에서 성공과 실패를 구분해서 처리할 수 있습니다.
generate_handler! 매크로에 새 커맨드 이름을 추가하는 것도 잊지 마세요.
프론트엔드에서 Rust 호출
프론트엔드에서 Rust 커맨드를 호출할 때는 @tauri-apps/api 패키지의 invoke 함수를 사용합니다.
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
function App() {
const [greeting, setGreeting] = useState("");
const [name, setName] = useState("");
async function handleGreet() {
const message = await invoke<string>("greet", { name });
setGreeting(message);
}
return (
<main>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요"
/>
<button onClick={handleGreet}>인사하기</button>
{greeting && <p>{greeting}</p>}
</main>
);
}
export default App;
invoke 함수의 첫 번째 인자는 Rust에서 정의한 커맨드 이름이고, 두 번째 인자는 매개변수를 담은 객체입니다.
Rust 함수의 매개변수 이름(name)과 객체의 키가 일치해야 합니다.
파일 읽기 커맨드도 비슷하게 호출할 수 있습니다.
async function handleReadFile() {
try {
const content = await invoke<string>("read_file", {
path: "/etc/hostname",
});
console.log(content);
} catch (error) {
console.error("파일 읽기 실패:", error);
}
}
Rust에서 Result의 Err를 반환하면 invoke가 reject되므로 try/catch로 에러를 처리합니다.
이벤트 시스템
invoke는 프론트엔드가 Rust를 호출하는 단방향 통신입니다.
반대로 Rust에서 프론트엔드로 데이터를 보내거나, 양방향으로 메시지를 주고받아야 할 때는 이벤트 시스템을 사용합니다.
Rust에서 이벤트를 발행하는 코드입니다.
use tauri::Emitter;
#[tauri::command]
fn start_timer(app: tauri::AppHandle) {
std::thread::spawn(move || {
for i in 1..=5 {
std::thread::sleep(std::time::Duration::from_secs(1));
app.emit("timer-tick", i).unwrap();
}
app.emit("timer-done", "완료!").unwrap();
});
}
프론트엔드에서 이벤트를 수신하는 코드입니다.
import { listen } from "@tauri-apps/api/event";
useEffect(() => {
const unlisten = listen<number>("timer-tick", (event) => {
console.log(`${event.payload}초 경과`);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
listen 함수가 정리(cleanup) 함수를 반환하니까 컴포넌트가 언마운트될 때 꼭 호출해 주세요.
앱 설정
src-tauri/tauri.conf.json 파일에서 앱의 메타데이터와 동작을 설정합니다.
{
"productName": "My Tauri App",
"version": "0.1.0",
"identifier": "com.my-tauri-app.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "bun run dev",
"beforeBuildCommand": "bun run build"
},
"app": {
"windows": [
{
"title": "My Tauri App",
"width": 800,
"height": 600
}
]
}
}
build.beforeDevCommand와 build.beforeBuildCommand에 프론트엔드 빌드 명령이 들어가 있어서 tauri dev나 tauri build를 실행하면 프론트엔드 빌드가 알아서 먼저 돌아갑니다.
Tauri 2에서는 보안을 위해 권한(capability) 시스템도 도입되었습니다.
src-tauri/capabilities/ 디렉토리의 JSON 파일에서 앱이 사용할 수 있는 API를 명시적으로 선언해야 합니다.
{
"identifier": "default",
"description": "기본 권한",
"windows": ["main"],
"permissions": ["core:default", "opener:default"]
}
프로덕션 빌드
앱을 배포하려면 프로덕션 빌드를 수행합니다.
bun run tauri build
프론트엔드를 빌드하고 Rust 코드를 릴리스 모드로 컴파일한 다음, 현재 OS에 맞는 설치 파일을 만들어 줍니다.
macOS에서는 .dmg와 .app이 생기고 Windows에서는 .msi와 .exe가 생깁니다.
Linux라면 .deb이나 .AppImage로 만들어져요.
결과물은 src-tauri/target/release/bundle/ 디렉토리에서 확인할 수 있습니다.
앱 크기를 보면 Electron과 얼마나 다른지 바로 체감하실 겁니다.
마치며
Tauri는 웹 개발 경험을 그대로 살리면서도 가볍고 빠른 데스크톱 앱을 만들 수 있게 해 줍니다. 프론트엔드는 React, Vue, Svelte 등 익숙한 도구를 쓰고 시스템 수준의 작업만 Rust로 처리하는 구조라서 웹 개발자라면 진입 장벽이 그렇게 높지 않아요.
Rust를 처음 접하는 분이라면 소유권이나 구조체 같은 기본 개념은 미리 익혀두시는 게 좋습니다. Tauri 2부터는 모바일 앱도 지원하니 하나의 코드베이스로 데스크톱과 모바일을 모두 커버하고 싶은 프로젝트에도 좋은 선택지가 될 수 있겠죠.
더 자세한 내용은 Tauri 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0