본문으로 건너뛰기
Previous
Next
Tauri 완전 가이드 | Electron보다 가볍고 빠른 데스크톱 앱 개발

Tauri 완전 가이드 | Electron보다 가볍고 빠른 데스크톱 앱 개발

Tauri 완전 가이드 | Electron보다 가볍고 빠른 데스크톱 앱 개발

이 글의 핵심

Electron의 강력한 대안 Tauri. Rust 기반으로 번들 크기는 10MB 이하, 메모리는 1/3만 사용하면서도 Windows·macOS·Linux를 모두 지원합니다. 웹 기술로 네이티브급 성능의 데스크톱 앱을 만드세요.

이 글의 핵심

Tauri는 Electron보다 10배 가볍고 빠른 크로스플랫폼 데스크톱 앱 프레임워크입니다. Rust 기반으로 번들 크기는 10MB 이하, 메모리는 Electron의 1/3만 사용하면서도 React·Vue·Svelte 등 익숙한 웹 기술로 개발할 수 있습니다.

목차

Tauri란?

Tauri는 2019년 시작된 오픈소스 프로젝트로, Rust + Web 기술로 네이티브 데스크톱 앱을 만드는 프레임워크입니다.

🚀 핵심 특징

1. 압도적으로 가벼움

Electron 앱:
- 번들 크기: 100MB+
- 메모리 사용: 150MB+
- Chromium 포함

Tauri 앱:
- 번들 크기: 10MB 이하
- 메모리 사용: 50MB 이하
- 시스템 WebView 사용

2. 크로스플랫폼

  • Windows (WebView2)
  • macOS (WKWebView)
  • Linux (WebKitGTK)
  • iOS/Android (Tauri Mobile - 베타)

3. 강력한 보안

// 모든 API는 명시적 허용 필요
{
  "tauri": {
    "allowlist": {
      "fs": {
        "readFile": true,
        "writeFile": false  // 명시적 거부
      }
    }
  }
}

4. 웹 기술 사용

  • React, Vue, Svelte, Angular 모두 지원
  • HTML/CSS/JavaScript로 UI 구성
  • Rust로 백엔드 로직 작성

Tauri 시작하기

1️⃣ 사전 요구사항

Windows

# Microsoft C++ Build Tools 설치
# https://visualstudio.microsoft.com/downloads/

# WebView2 (Windows 10+ 기본 포함)

macOS

# Xcode Command Line Tools 설치
xcode-select --install

# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
    build-essential \
    curl \
    wget \
    libssl-dev \
    libgtk-3-dev \
    libayatana-appindicator3-dev \
    librsvg2-dev

# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

2️⃣ 프로젝트 생성

# Tauri CLI 설치
npm install -g @tauri-apps/cli

# 새 프로젝트 생성 (React 템플릿)
npm create tauri-app@latest

# 프로젝트 이름: my-tauri-app
# 프론트엔드: React
# TypeScript: Yes
# Package Manager: npm

cd my-tauri-app
npm install

3️⃣ 프로젝트 구조

my-tauri-app/
├── src/               # React 프론트엔드
│   ├── App.tsx
│   └── main.tsx
├── src-tauri/         # Rust 백엔드
│   ├── src/
│   │   └── main.rs    # Rust 진입점
│   ├── tauri.conf.json  # Tauri 설정
│   └── Cargo.toml     # Rust 의존성
└── package.json

개발 서버 실행

# 개발 모드 (핫 리로드)
npm run tauri dev

# 빌드 (배포용)
npm run tauri build

Tauri API 사용하기

📁 파일 시스템 읽기

프론트엔드 (React)

// src/App.tsx
import { open } from '@tauri-apps/api/dialog';
import { readTextFile } from '@tauri-apps/api/fs';
import { useState } from 'react';

function App() {
  const [content, setContent] = useState('');

  const handleOpenFile = async () => {
    try {
      // 파일 선택 대화상자
      const selected = await open({
        multiple: false,
        filters: [{
          name: 'Text',
          extensions: ['txt', 'md']
        }]
      });

      if (selected) {
        // 파일 읽기
        const text = await readTextFile(selected as string);
        setContent(text);
      }
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <button onClick={handleOpenFile}>Open File</button>
      <pre>{content}</pre>
    </div>
  );
}

export default App;

권한 설정 (src-tauri/tauri.conf.json)

{
  "tauri": {
    "allowlist": {
      "dialog": {
        "open": true
      },
      "fs": {
        "readFile": true,
        "scope": ["**"]
      }
    }
  }
}

💾 파일 저장

import { save } from '@tauri-apps/api/dialog';
import { writeTextFile } from '@tauri-apps/api/fs';

const handleSaveFile = async () => {
  try {
    const filePath = await save({
      filters: [{
        name: 'Text',
        extensions: ['txt']
      }]
    });

    if (filePath) {
      await writeTextFile(filePath, content);
      console.log('File saved!');
    }
  } catch (error) {
    console.error(error);
  }
};

🔔 시스템 알림

import { sendNotification } from '@tauri-apps/api/notification';

const handleNotify = async () => {
  await sendNotification({
    title: 'Tauri App',
    body: 'Hello from Tauri!',
  });
};

🪟 윈도우 제어

import { appWindow } from '@tauri-apps/api/window';

// 윈도우 최소화
await appWindow.minimize();

// 윈도우 최대화
await appWindow.maximize();

// 윈도우 닫기
await appWindow.close();

// 윈도우 숨기기
await appWindow.hide();

// 윈도우 크기 변경
await appWindow.setSize({ width: 800, height: 600 });

// 윈도우 타이틀 변경
await appWindow.setTitle('My App');

Rust 백엔드 함수 호출

1️⃣ Rust 함수 정의

// src-tauri/src/main.rs

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// 프론트엔드에서 호출할 함수
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[tauri::command]
fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    // HTTP 요청 (예시)
    match reqwest::get(&url).await {
        Ok(response) => match response.text().await {
            Ok(body) => Ok(body),
            Err(e) => Err(e.to_string()),
        },
        Err(e) => Err(e.to_string()),
    }
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            greet,
            add_numbers,
            fetch_data
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

2️⃣ 프론트엔드에서 호출

// src/App.tsx
import { invoke } from '@tauri-apps/api/tauri';
import { useState } from 'react';

function App() {
  const [result, setResult] = useState('');

  const handleGreet = async () => {
    const message = await invoke<string>('greet', { name: 'Alice' });
    setResult(message);
  };

  const handleAdd = async () => {
    const sum = await invoke<number>('add_numbers', { a: 10, b: 20 });
    setResult(`Sum: ${sum}`);
  };

  const handleFetch = async () => {
    try {
      const data = await invoke<string>('fetch_data', { 
        url: 'https://api.github.com/users/tauri-apps' 
      });
      setResult(data);
    } catch (error) {
      setResult(`Error: ${error}`);
    }
  };

  return (
    <div>
      <button onClick={handleGreet}>Greet</button>
      <button onClick={handleAdd}>Add</button>
      <button onClick={handleFetch}>Fetch</button>
      <div>{result}</div>
    </div>
  );
}

실전 프로젝트: 메모장 앱

기능 명세

  • ✅ 파일 열기/저장
  • ✅ 자동 저장
  • ✅ 탭 지원
  • ✅ 다크 모드
  • ✅ 찾기/바꾸기

Rust 백엔드 (자동 저장)

// src-tauri/src/main.rs
use std::fs;
use std::path::Path;

#[tauri::command]
fn auto_save(path: String, content: String) -> Result<(), String> {
    fs::write(&path, content)
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
fn get_recent_files() -> Vec<String> {
    // 최근 파일 목록 (실제로는 DB나 파일에 저장)
    vec![
        "/path/to/file1.txt".to_string(),
        "/path/to/file2.txt".to_string(),
    ]
}

React 프론트엔드

// src/App.tsx
import { useState, useEffect } from 'react';
import { open, save } from '@tauri-apps/api/dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import { invoke } from '@tauri-apps/api/tauri';

interface Tab {
  id: string;
  path: string | null;
  content: string;
  saved: boolean;
}

function App() {
  const [tabs, setTabs] = useState<Tab[]>([
    { id: '1', path: null, content: '', saved: true }
  ]);
  const [activeTab, setActiveTab] = useState('1');

  // 자동 저장 (2초마다)
  useEffect(() => {
    const interval = setInterval(async () => {
      const tab = tabs.find(t => t.id === activeTab);
      if (tab && tab.path && !tab.saved) {
        try {
          await invoke('auto_save', {
            path: tab.path,
            content: tab.content
          });
          updateTab(activeTab, { saved: true });
        } catch (error) {
          console.error('Auto-save failed:', error);
        }
      }
    }, 2000);

    return () => clearInterval(interval);
  }, [tabs, activeTab]);

  const handleOpen = async () => {
    const selected = await open({
      multiple: false,
      filters: [{ name: 'Text', extensions: ['txt', 'md'] }]
    });

    if (selected) {
      const content = await readTextFile(selected as string);
      const newTab: Tab = {
        id: Date.now().toString(),
        path: selected as string,
        content,
        saved: true
      };
      setTabs([...tabs, newTab]);
      setActiveTab(newTab.id);
    }
  };

  const handleSave = async () => {
    const tab = tabs.find(t => t.id === activeTab);
    if (!tab) return;

    const filePath = tab.path || await save({
      filters: [{ name: 'Text', extensions: ['txt'] }]
    });

    if (filePath) {
      await writeTextFile(filePath, tab.content);
      updateTab(activeTab, { path: filePath, saved: true });
    }
  };

  const updateTab = (id: string, updates: Partial<Tab>) => {
    setTabs(tabs.map(t => t.id === id ? { ...t, ...updates } : t));
  };

  const currentTab = tabs.find(t => t.id === activeTab);

  return (
    <div className="app">
      <div className="toolbar">
        <button onClick={handleOpen}>Open</button>
        <button onClick={handleSave}>Save</button>
        <button onClick={() => setTabs([...tabs, {
          id: Date.now().toString(),
          path: null,
          content: '',
          saved: true
        }])}>New Tab</button>
      </div>

      <div className="tabs">
        {tabs.map(tab => (
          <div
            key={tab.id}
            className={`tab ${activeTab === tab.id ? 'active' : ''}`}
            onClick={() => setActiveTab(tab.id)}
          >
            {tab.path ? tab.path.split('/').pop() : 'Untitled'}
            {!tab.saved && ' *'}
            <button onClick={() => setTabs(tabs.filter(t => t.id !== tab.id))}>
              ×
            </button>
          </div>
        ))}
      </div>

      {currentTab && (
        <textarea
          value={currentTab.content}
          onChange={(e) => updateTab(activeTab, {
            content: e.target.value,
            saved: false
          })}
          placeholder="Start typing..."
        />
      )}
    </div>
  );
}

export default App;

빌드 및 배포

개발 빌드

npm run tauri build

결과물:

  • Windows: .exe (src-tauri/target/release)
  • macOS: .app, .dmg (src-tauri/target/release/bundle)
  • Linux: .deb, .AppImage (src-tauri/target/release/bundle)

릴리스 최적화

// src-tauri/tauri.conf.json
{
  "tauri": {
    "bundle": {
      "identifier": "com.myapp.dev",
      "icon": [
        "icons/32x32.png",
        "icons/128x128.png",
        "icons/icon.icns",
        "icons/icon.ico"
      ],
      "resources": [],
      "copyright": "",
      "category": "Utility",
      "shortDescription": "My awesome app",
      "longDescription": "Detailed description..."
    }
  }
}

자동 업데이트

// src-tauri/src/main.rs
use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let handle = app.handle();
            tauri::async_runtime::spawn(async move {
                // 업데이트 확인
                match handle.updater().check().await {
                    Ok(update) => {
                        if update.is_update_available() {
                            update.download_and_install().await.unwrap();
                        }
                    }
                    Err(e) => println!("Failed to check for updates: {}", e),
                }
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Tauri vs Electron

항목TauriElectron
번들 크기10MB 이하100MB+
메모리50MB150MB+
시작 시간<1초2-3초
언어Rust + WebJavaScript + Web
WebView시스템 기본Chromium 내장
보안기본적으로 강력추가 설정 필요
크로스플랫폼
생태계🌱 성장 중🌳 성숙

Tauri Mobile (iOS/Android)

모바일 앱 개발 (베타)

# iOS 타겟 추가
npm run tauri ios init
npm run tauri ios dev

# Android 타겟 추가
npm run tauri android init
npm run tauri android dev

하나의 코드베이스로 5개 플랫폼 지원:

  • Windows
  • macOS
  • Linux
  • iOS
  • Android

실제 사용 사례

1. VS Code 대체 에디터

  • Zed: 고성능 코드 에디터
  • Lapce: Rust 기반 에디터

2. 개발 도구

  • DevToys: 개발자 유틸리티 모음
  • Clash Verge: 네트워크 프록시 GUI

3. 생산성 도구

  • Pomatez: 포모도로 타이머
  • Icalingua: 채팅 클라이언트

자주 묻는 질문 (FAQ)

Q1. Tauri 앱의 실제 성능은 어떤가요?

A: 벤치마크 결과:

  • 시작 시간: Electron 2.5초 → Tauri 0.8초
  • 메모리 사용: Electron 150MB → Tauri 45MB
  • 번들 크기: Electron 120MB → Tauri 8MB

Q2. 시스템 WebView를 사용하면 브라우저 차이 문제가 있지 않나요?

A: 맞습니다. 하지만:

  • Windows: WebView2 (Chromium 기반, 자동 업데이트)
  • macOS: WKWebView (Safari 기반, 최신 표준 지원)
  • Linux: WebKitGTK (최신 버전 권장)

대부분의 모던 웹 기술은 문제없이 작동합니다.

Q3. Rust를 모르는데 Tauri를 배울 수 있나요?

A: 네! 대부분의 로직은 프론트엔드(React/Vue)에서 작성하고, Rust는:

  • 템플릿 코드 사용 (복붙만 해도 됨)
  • 필요한 경우에만 커스텀 함수 작성
  • ChatGPT로 Rust 코드 생성 가능

Q4. Electron에서 Tauri로 마이그레이션할 수 있나요?

A: 가능하지만 노력이 필요합니다.

  • UI 코드: 그대로 사용 가능
  • Node.js API: Tauri API로 재작성 필요
  • 네이티브 모듈: Rust로 재작성

Q5. Tauri의 단점은 무엇인가요?

A:

  • 생태계: Electron보다 작음 (하지만 빠르게 성장 중)
  • Rust 학습 곡선: 고급 기능은 Rust 지식 필요
  • 디버깅: Electron보다 복잡할 수 있음

핵심 정리

Tauri의 장점

  1. 압도적으로 가벼움: 번들 10MB, 메모리 50MB
  2. 빠른 성능: 네이티브급 시작 시간
  3. 강력한 보안: 기본적으로 안전한 설계
  4. 웹 기술 사용: React/Vue/Svelte
  5. 크로스플랫폼: Windows/macOS/Linux/iOS/Android

🚀 다음 단계


시작하기: npm create tauri-app@latest로 5분 만에 프로젝트를 시작하고, Electron보다 10배 가벼운 데스크톱 앱을 만드세요! 🚀