Tauri v2 자동 업데이트 가이드
작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Tauri v2 데스크탑 앱 자동 업데이트 (Windows / macOS / Linux) 공통 참조:
../common/update.md(업데이트 전략, 강제 업데이트, 롤아웃, UX) 공통 참조:../common/ci-cd.md(CI/CD 파이프라인, 버전 관리)
목차
- tauri-plugin-updater 설정
- 업데이트 서버 구성
- Ed25519 서명 키 관리
- 업데이트 매니페스트 JSON
- 플랫폼별 업데이트 흐름
- 점진적 롤아웃
- 강제 업데이트 + 최소 버전 체크
- CI/CD 파이프라인 연동
1. tauri-plugin-updater 설정
1.1 설치
# Rust 의존성
cargo add tauri-plugin-updater
# Frontend 의존성
npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process
1.2 플러그인 등록 (Rust)
// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
// ... 기타 플러그인
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
1.3 tauri.conf.json 설정
{
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"endpoints": [
"https://releases.example.com/tauri/{{target}}/{{arch}}/{{current_version}}"
],
"windows": {
"installMode": "passive"
}
}
}
}
설정 필드 상세
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
pubkey | string | O | Ed25519 공개 키 (업데이트 서명 검증) |
endpoints | string[] | O | 업데이트 매니페스트 URL (복수 가능, fallback) |
windows.installMode | string | X | passive (기본, 자동) / basicUi (진행률 UI) / quiet (완전 무음) |
엔드포인트 URL 템플릿 변수
| 변수 | 설명 | 예시 값 |
|---|---|---|
{{target}} | OS 타겟 | linux, darwin, windows |
{{arch}} | CPU 아키텍처 | x86_64, aarch64 |
{{current_version}} | 현재 앱 버전 | 1.2.3 |
1.4 Capability 설정
// src-tauri/capabilities/default.json
{
"permissions": [
"core:default",
"updater:default",
"process:allow-restart"
]
}
2. 업데이트 서버 구성
2.1 서버 옵션 비교
| 옵션 | 비용 | 난이도 | 롤아웃 | 분석 | 추천 대상 |
|---|---|---|---|---|---|
| GitHub Releases | 무료 | 쉬움 | X | X | 오픈소스, 소규모 |
| Cloudflare R2 + Worker | 거의 무료 | 중간 | O | O | 중규모 |
| S3 + CloudFront + Lambda | 저렴 | 중간 | O | O | 중~대규모 |
| 자체 서버 (Laravel/Go) | 서버 비용 | 높음 | O | O | 완전 제어 필요 시 |
2.2 GitHub Releases (가장 간단)
// tauri.conf.json
{
"plugins": {
"updater": {
"endpoints": [
"https://github.com/{owner}/{repo}/releases/latest/download/latest.json"
]
}
}
}
tauri-actionGitHub Action이 빌드 시latest.json매니페스트를 자동 생성하여 Release에 첨부한다.
2.3 자체 업데이트 서버 (Laravel)
// routes/api.php
Route::get('/tauri/{target}/{arch}/{current_version}', [TauriUpdateController::class, 'check']);
// app/Http/Controllers/TauriUpdateController.php
class TauriUpdateController extends Controller
{
public function check(
string $target,
string $arch,
string $currentVersion
): JsonResponse {
$release = TauriRelease::query()
->where('target', $target)
->where('arch', $arch)
->where('active', true)
->latest('version')
->first();
// 업데이트 없음
if (!$release || version_compare($currentVersion, $release->version, '>=')) {
return response()->json(null, 204);
}
// 점진적 롤아웃 체크
if ($release->rollout_percentage < 100) {
$bucket = crc32(request()->ip()) % 100;
if ($bucket >= $release->rollout_percentage) {
return response()->json(null, 204);
}
}
return response()->json([
'version' => $release->version,
'notes' => $release->release_notes,
'pub_date' => $release->published_at->toIso8601String(),
'url' => $release->download_url,
'signature' => $release->signature,
]);
}
}
// Database Migration
Schema::create('tauri_releases', function (Blueprint $table) {
$table->id();
$table->string('version'); // "2.4.0"
$table->string('target'); // "darwin", "linux", "windows"
$table->string('arch'); // "x86_64", "aarch64"
$table->string('download_url'); // 바이너리 다운로드 URL
$table->text('signature'); // Ed25519 서명
$table->text('release_notes')->nullable();
$table->timestamp('published_at');
$table->boolean('active')->default(true);
$table->unsignedTinyInteger('rollout_percentage')->default(100);
$table->timestamps();
$table->unique(['version', 'target', 'arch']);
$table->index(['target', 'arch', 'active']);
});
2.4 Cloudflare R2 + Worker
// Cloudflare Worker - 업데이트 체 크 엔드포인트
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const [, target, arch, currentVersion] =
url.pathname.match(/\/tauri\/(\w+)\/([\w_]+)\/([\d.]+)/) ?? [];
if (!target || !arch || !currentVersion) {
return new Response('Bad Request', { status: 400 });
}
// R2에서 매니페스트 읽기
const manifestKey = `releases/latest/${target}-${arch}.json`;
const manifest = await env.R2_BUCKET.get(manifestKey);
if (!manifest) {
return new Response(null, { status: 204 });
}
const release = JSON.parse(await manifest.text());
// 현재 버전이 최신이면 204
if (compareVersions(currentVersion, release.version) >= 0) {
return new Response(null, { status: 204 });
}
return new Response(JSON.stringify(release), {
headers: { 'Content-Type': 'application/json' },
});
},
};
3. Ed25519 서명 키 관리
3.1 키 생성
# Tauri CLI로 키 쌍 생성
npx tauri signer generate -w ~/.tauri/myapp.key
# 출력:
# 비밀 키 → ~/.tauri/myapp.key (절대 공개 금지)
# 공개 키 → 콘솔에 출력됨 → tauri.conf.json의 pubkey에 입력
3.2 키 저장소 관리
| 키 | 저장 위치 | 접근 권한 |
|---|---|---|
| 비밀 키 (signing key) | GitHub Secrets TAURI_SIGNING_PRIVATE_KEY | CI/CD만 |
| 비밀 키 패스워드 | GitHub Secrets TAURI_SIGNING_PRIVATE_KEY_PASSWORD | CI/CD만 |
| 공개 키 (verify key) | tauri.conf.json > plugins.updater.pubkey | 앱에 내장 (공개) |
3.3 환경 변수 설정
# 로컬 빌드 시
export TAURI_SIGNING_PRIVATE_KEY=$(cat ~/.tauri/myapp.key)
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your-password"
# GitHub Actions
# Settings > Secrets > Actions에 등록
3.4 키 로테이션 절차
1. 새 키 쌍 생성 (tauri signer generate)
2. tauri.conf.json의 pubkey를 새 공개 키로 변경
3. 새 키로 서명된 업데이트 배포
4. 이전 키로 서명된 바이너리는 검증 실패 → 사용자가 수동 다운로드 필요
키 로테이션은 Breaking Change이다. 기존 사용자는 자동 업데이트가 실패하므로, 앱 내에서 "수동 다운로드" 링크를 안내해야 한다.
키 관리 체크리스트
- Ed25519 키 쌍 생성
- 비밀 키를 GitHub Secrets에 등록
- 비밀 키 패스워드를 GitHub Secrets에 등록
- 공개 키를
tauri.conf.json에 설정 - 비밀 키 백업 (안전한 장소, 예: 1Password/Vault)
- 키 로테이션 시 사용자 안내 방안 준비
4. 업데이트 매니페스트 JSON
4.1 매니페스트 구조
{
"version": "2.4.0",
"notes": "Bug fixes and performance improvements\n\n- Fixed crash on startup\n- Improved rendering speed",
"pub_date": "2026-04-06T12:00:00Z",
"platforms": {
"darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-aarch64.app.tar.gz"
},
"darwin-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-x86_64.app.tar.gz"
},
"linux-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-x86_64.AppImage"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQgLi4u...",
"url": "https://releases.example.com/v2.4.0/myapp-2.4.0-x64-setup.nsis.zip"
}
}
}
4.2 플랫폼 키 명명 규칙
| 플랫폼 키 | OS | 아키텍처 | 빌드 출력 |
|---|---|---|---|
darwin-aarch64 | macOS | Apple Silicon | .app.tar.gz |
darwin-x86_64 | macOS | Intel | .app.tar.gz |
linux-x86_64 | Linux | x64 | .AppImage |
linux-aarch64 | Linux | ARM64 | .AppImage |
windows-x86_64 | Windows | x64 | .nsis.zip 또는 .msi.zip |
windows-aarch64 | Windows | ARM64 | .nsis.zip |
4.3 단일 엔드포인트 vs 플랫폼별 엔드포인트
단일 엔드포인트 (권장 - 모든 플랫폼을 하나의 JSON에):
// tauri.conf.json
"endpoints": [
"https://releases.example.com/tauri/latest.json"
]
플랫폼별 엔드포인트 (동적 라우팅):
// tauri.conf.json
"endpoints": [
"https://releases.example.com/tauri/{{target}}/{{arch}}/{{current_version}}"
]
단일 엔드포인트: CDN 캐싱 용이, 관리 간단. 플랫폼별 엔드포인트: 서버에서 롤아웃/A-B 테스트 등 세밀한 제어 가능.
4.4 서명 파일 구조
Tauri 빌드 시 서명 파일이 바이너리와 함께 생성된다:
target/release/bundle/
+-- nsis/
| +-- myapp_2.4.0_x64-setup.exe # 설치 프로그램
| +-- myapp_2.4.0_x64-setup.nsis.zip # 업데이트용 (압축)
| +-- myapp_2.4.0_x64-setup.nsis.zip.sig # Ed25519 서명
+-- macos/
| +-- myapp.app.tar.gz # 업데이트용 (압축)
| +-- myapp.app.tar.gz.sig # Ed25519 서명
+-- appimage/
+-- myapp_2.4.0_amd64.AppImage # AppImage
+-- myapp_2.4.0_amd64.AppImage.sig # Ed25519 서명
5. 플랫폼별 업데이트 흐름
5.1 공통 업데이트 플로우 (Frontend)
// src/lib/services/updater.ts
import { check, type Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
export interface UpdateProgress {
status: 'checking' | 'available' | 'downloading' | 'installing' | 'up-to-date' | 'error';
version?: string;
notes?: string;
downloadedBytes?: number;
totalBytes?: number;
error?: string;
}
export async function checkAndInstallUpdate(
onProgress: (progress: UpdateProgress) => void,
): Promise<void> {
onProgress({ status: 'checking' });
try {
const update = await check();
if (!update) {
onProgress({ status: 'up-to-date' });
return;
}
onProgress({
status: 'available',
version: update.version,
notes: update.body ?? undefined,
});
// 사용자 확인 대기 (UI에서 처리)
// 아래 코드는 사용자가 "업데이트" 버튼 클릭 후 호출
await downloadAndInstall(update, onProgress);
} catch (error) {
onProgress({
status: 'error',
error: error instanceof Error ? error.message : String(error),
});
}
}
export async function downloadAndInstall(
update: Update,
onProgress: (progress: UpdateProgress) => void,
): Promise<void> {
let totalBytes = 0;
let downloadedBytes = 0;
await update.downloadAndInstall((event) => {
if (event.event === 'Started') {
totalBytes = event.data.contentLength ?? 0;
onProgress({
status: 'downloading',
version: update.version,
downloadedBytes: 0,
totalBytes,
});
} else if (event.event === 'Progress') {
downloadedBytes += event.data.chunkLength;
onProgress({
status: 'downloading',
version: update.version,
downloadedBytes,
totalBytes,
});
} else if (event.event === 'Finished') {
onProgress({ status: 'installing', version: update.version });
}
});
// 재시작
await relaunch();
}
5.2 백그라운드 자동 체크
// src/lib/services/auto-updater.ts
import { check } from '@tauri-apps/plugin-updater';
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4시간
let checkInterval: ReturnType<typeof setInterval> | null = null;
export function startAutoUpdateCheck(
onUpdateAvailable: (version: string, notes: string | null) => void,
): void {
// 앱 시작 후 30초 뒤 첫 체크 (시작 성능 영향 최소화)
setTimeout(async () => {
await performCheck(onUpdateAvailable);
}, 30_000);
// 이후 주기적 체크
checkInterval = setInterval(async () => {
await performCheck(onUpdateAvailable);
}, CHECK_INTERVAL_MS);
}
export function stopAutoUpdateCheck(): void {
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
}
async function performCheck(
onUpdateAvailable: (version: string, notes: string | null) => void,
): Promise<void> {
try {
const update = await check();
if (update) {
onUpdateAvailable(update.version, update.body);
}
} catch {
// 네트워크 에러 등은 무시 (자동 체크이므로)
}
}
5.3 플랫폼별 특이사항
| OS | 업데이트 바이너리 | 설치 방식 | 재시작 필요 | 비고 |
|---|---|---|---|---|
| Windows (NSIS) | .nsis.zip | NSIS 인스톨러 실행 | O | installMode: passive/basicUi/quiet |
| Windows (MSI) | .msi.zip | MSI 자동 실행 | O | WiX 기반 |
| macOS | .app.tar.gz | 앱 번들 교체 | O | Gatekeeper 재검증 없음 (동일 서명) |
| Linux | .AppImage | 파일 교체 | O | AppImage 자체 업데이트 |
Windows installMode 비교
| 모드 | 설명 | 사용자 경험 |
|---|---|---|
passive | 자동 설치 (진행 바 표시) | 기본값, 대부분 상황에 적합 |
basicUi | NSIS 기본 UI 표시 | 사용자가 설치 과정 확인 가능 |
quiet | 완전 무음 설치 | 백그라운드, 사용자 인지 없음 |
6. 점진적 롤아웃
공통 롤아웃 전략:
../common/update.md참조.
자체 업데이트 서버에서 롤아웃 구현
// TauriUpdateController.php - 롤아웃 로직
public function check(string $target, string $arch, string $currentVersion): JsonResponse
{
$release = TauriRelease::query()
->where('target', $target)
->where('arch', $arch)
->where('active', true)
->latest('version')
->first();
if (!$release || version_compare($currentVersion, $release->version, '>=')) {
return response()->json(null, 204);
}
// 롤아웃 퍼센트 체크
if (!$this->isInRolloutGroup($release->rollout_percentage)) {
return response()->json(null, 204); // 아직 대상 아님
}
return response()->json([
'version' => $release->version,
'notes' => $release->release_notes,
'pub_date' => $release->published_at->toIso8601String(),
'url' => $release->download_url,
'signature' => $release->signature,
]);
}
private function isInRolloutGroup(int $percentage): bool
{
if ($percentage >= 100) return true;
// IP 또는 요청 헤더 기반 결정론적 버킷팅
$identifier = request()->ip() . request()->header('X-Machine-Id', '');
$bucket = crc32($identifier) % 100;
return $bucket < $percentage;
}
롤아웃 관리 API
// 관리자가 롤아웃 퍼센트 조절
Route::put('/admin/releases/{id}/rollout', function (Request $request, int $id) {
$release = TauriRelease::findOrFail($id);
$release->update([
'rollout_percentage' => $request->input('percentage'), // 0~100
]);
return response()->json(['message' => 'Rollout updated']);
});