Rust 오류 처리 in Tauri Commands — 실제로 작동하는 패턴
Source: Dev.to

모든 테스트는 8년 된 MacBook Air에서 실행되었습니다.
솔로 개발자로서 7개의 Mac 앱을 배포한 결과입니다. 스폰서된 의견이 아닙니다.
제가 처음 배포한 Tauri 앱은 오류 처리가 일관되지 않았습니다. 일부 커맨드는 문자열을 반환했고, 일부는 패닉을 일으켰으며, 일부는 오류를 조용히 무시했습니다.
아래는 7개의 앱을 만든 뒤 제가 채택한 패턴입니다.
순진한 오류 처리의 문제점
Tauri 커맨드는 Result를 반환하며, 여기서 E는 serde::Serialize를 구현해야 합니다. 오류에 대해 그냥 String을 반환하고 싶어지는 유혹이 있습니다:
#[tauri::command]
fn do_something() -> Result {
some_operation().map_err(|e| e.to_string())
}
이렇게 하면 동작하지만 규모가 커지면 엉망이 됩니다:
- 프론트엔드는 타입이 없는 문자열을 받습니다.
- 오류 타입별 매칭이 불가능합니다.
- 로깅이 일관되지 않습니다.
- 오류 메시지는
.to_string()이 반환하는 그대로입니다.
실제로 작동하는 패턴
앱 전체에서 사용하는 단일 오류 타입
#[derive(Debug, thiserror::Error, serde::Serialize)]
#[serde(tag = "kind", content = "message")]
pub enum AppError {
#[error("IO error: {0}")]
Io(String),
#[error("ADB error: {0}")]
Adb(String),
#[error("Database error: {0}")]
Database(String),
#[error("Permission denied: {0}")]
Permission(String),
}
impl From for AppError {
fn from(e: std::io::Error) -> Self {
AppError::Io(e.to_string())
}
}
모든 커맨드는 Result를 반환합니다. 프론트엔드는 kind와 message 필드를 가진 타입이 지정된 오류 객체를 받아, TypeScript에서 kind를 매칭해 각 오류 타입에 맞는 UI를 표시할 수 있습니다.
프론트엔드 측
try {
await invoke('do_something')
} catch (e: any) {
if (e.kind === 'Permission') {
showPermissionDialog()
} else if (e.kind === 'Adb') {
showAdbTroubleshooting()
} else {
showGenericError(e.message)
}
}
양쪽 모두 타입이 지정된 오류. 문자열 파싱이 필요 없고, 무엇이 잘못됐는지 추측할 필요도 없습니다.
로깅 레이어
비즈니스 로직에 흩어놓지 말고 커맨드 경계에서 로깅을 추가합니다:
#[tauri::command]
async fn sync_files(handle: AppHandle) -> Result {
sync_files_inner(&handle).await.map_err(|e| {
log::error!("sync_files failed: {:?}", e);
e
})
}
- 커맨드 실패당 한 줄의 로그.
- 포맷이 일관됨.
- 프로덕션 로그에서 찾기 쉬움.
결론
thiserror + 태그가 달린 enum 패턴은 Tauri 앱 오류 처리를 위한 올바른 기본값입니다. 첫 날부터 설정하세요. 이미 배포된 앱에 일관된 오류 처리를 뒤늦게 적용하는 것은 고통스럽습니다.
String 오류 단축키는 프로토타입에는 괜찮지만, 실제 사용자에게 배포할 앱에는 적합하지 않습니다.
이 글이 도움이 되었다면 ❤️ 하나가 생각보다 큰 힘이 됩니다 — 감사합니다!
링크
- Hiyoko PDF Vault →
- X →