프론트엔드에서 Rust 호출하기
이 문서는 애플리케이션의 프론트엔드에서 Rust 코드와 통신하는 방법에 대한 지침을 담고 있습니다. Rust 코드에서 프론트엔드와 통신하는 방법에 대해서는 이전 절의 Rust에서 프론트엔드 호출하기를 참조하십시오.
Tauri는 더 동적인 이벤트 시스템과 함께 형식 안전성을 갖춘 Rust 함수에 액세스하기 위한 명령 프리미티브를 제공합니다.
Tauri는 웹 앱에서 Rust 함수를 호출하기 위한 간단하면서도 강력한 “명령 command
” 시스템을 제공합니다.
“명령”은 인수를 받아들이고 값을 반환할 수 있습니다. 또한 오류를 반환하거나 async
(비동기)로 만들 수도 있습니다.
명령은 src-tauri/src/lib.rs
파일에서 정의할 수 있습니다.
명령을 만들려면 함수를 추가하고 #[tauri::command]
로 주석을 달기만 하면 됩니다:
#[tauri::command]fn my_custom_command() { println!("I was invoked from JavaScript!");}
명령 목록은 다음과 같이 “빌더” 함수에 제공해야 합니다.
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
이제 JavaScript 코드에서 명령을 호출할 수 있습니다.
// Tauri API의 npm 패키지를 사용하는 경우:import { invoke } from '@tauri-apps/api/core';
// Tauri global script를 사용하는 경우(npm 패키지를 사용하지 않을 때)// `tauri.conf.json`의 `app.withGlobalTauri`를 반드시 true로 설정하십시오.const invoke = window.__TAURI__.core.invoke;
// 명령을 호출합니다invoke('my_custom_command');
애플리케이션에서 많은 구성 요소를 정의하거나 구성 요소를 그룹화할 수 있는 경우, lib.rs
파일에 명령 정의를 채워 넣는 대신 다른 모듈에 명령을 정의할 수 있습니다.
예를 들어, src-tauri/src/commands.rs
파일에 명령을 정의해 보겠습니다.
#[tauri::command]pub fn my_custom_command() { println!("I was invoked from JavaScript!");}
lib.rs
파일에서는 모듈을 정의하고 그에 따른 명령 목록을 제공합니다:
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![commands::my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
명령 목록의 “접두사 commands::
”에 유의하십시오. 이는 이 명령 함수에 대한 전체 경로를 의미합니다.
이 예의 명령 이름은 my_custom_command
이므로 프론트엔드 내에서 invoke("my_custom_command")
를 실행하여 호출할 수 있으며, 접두사 commands::
는 무시됩니다.
Rust 프론트엔드를 사용하여 인수 없이 invoke()
를 호출하는 경우, 프론트엔드 코드를 다음과 같이 변경해야 합니다.
그 이유는 Rust가 선택적 인수를 지원하지 않기 때문입니다.
#[wasm_bindgen]extern "C" { // 인수 없이 호출 #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)] async fn invoke_without_args(cmd: &str) -> JsValue;
// 인수가 있는 호출(기본값) #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] async fn invoke(cmd: &str, args: JsValue) -> JsValue;
// 이 둘은 다른 이름이어야 합니다!}
명령 핸들러는 인수를 받을 수 있습니다:
#[tauri::command]fn my_custom_command(invoke_message: String) { println!("I was invoked from JavaScript, with this message: {}", invoke_message);}
인수는 camelCase 키를 가진 JSON 객체로 전달해야 합니다.
《번역 주》 영어 복합어 표기법에 대하여: ・camelCase “카멜 케이스” 연결된 단어의 첫 글자를 대문자로 쓰는 표기법 ・snake_case “스네이크 케이스” 연결할 단어 사이에 밑줄(_)을 넣고 모두 소문자로 쓰는 표기법
invoke('my_custom_command', { invokeMessage: 'Hello!' });
인수는 serde::Deserialize
를 구현하는 한 어떤 형식이든 가능합니다.
해당 JavaScript:
invoke('my_custom_command', { invoke_message: 'Hello!' });
명령 핸들러도 데이터를 반환합니다:
#[tauri::command]fn my_custom_command() -> String { "Hello from Rust!".into()}
invoke
함수(“호출” 함수)는 반환 값에 따라 처리 내용이 결정되는 “프로미스”를 반환합니다:
invoke('my_custom_command').then((message) => console.log(message));
반환되는 데이터는 serde::Serialize
를 구현하는 한 어떤 형식이든 상관없습니다.
serde::Serialize
를 구현하는 반환 값은 응답이 프론트엔드로 전송될 때 JSON 형식으로 직렬화됩니다.
이 방법에서는 파일이나 다운로드 HTTP 응답과 같은 큰 데이터를 반환하려고 하면 애플리케이션 속도가 저하될 수 있습니다.
배열 버퍼를 최적화된 방법으로 반환하려면 tauri::ipc::Response
를 사용합니다.
《번역 주》 직렬화 serialize: 여러 병렬 데이터를 “직렬화”(한 줄로 나열하는 작업)하여 파일 저장이나 네트워크 송수신이 가능하도록 변환하는 처리.
use tauri::ipc::Response;#[tauri::command]fn read_file() -> Response { let data = std::fs::read("/path/to/file").unwrap(); tauri::ipc::Response::new(data)}
핸들러가 처리에 실패하여 오류를 반환해야 하는 경우, 명령 함수에 Result
를 반환하게 합니다.
#[tauri::command]fn login(user: String, password: String) -> Result<String, String> { if user == "tauri" && password == "tauri" { // 처리 성공 Ok("logged_in".to_string()) } else { // 처리 실패 Err("invalid credentials".to_string()) }}
명령이 오류를 반환하는 경우, “프로미스”는 “처리 실패”, 그렇지 않은 경우에는 “처리 성공”이 됩니다:
invoke('login', { user: 'tauri', password: '0j4rijw8=' }) .then((message) => console.log(message)) .catch((error) => console.error(error));
위에서 언급했듯이, 오류를 포함하여 명령에서 반환되는 모든 것은 serde::Serialize
를 구현해야 합니다.
그러나 Rust 표준 라이브러리 또는 외부 크레이트의 오류 유형을 사용하는 경우, 대부분의 오류 유형은 serde::Serialize
를 구현하지 않으므로 문제가 발생할 수 있습니다.
이 문제를 해결하는 간단한 시나리오는 map_err
를 사용하여 이러한 오류를 String
으로 변환하는 것입니다.
#[tauri::command]fn my_custom_command() -> Result<(), String> { std::fs::File::open("path/to/file").map_err(|err| err.to_string())?; // 성공하면 `null`을 반환합니다 Ok(())}
이 해결책은 그다지 일반적이지 않으므로 serde::Serialize
를 구현하는 고유한 오류 유형을 만드는 것이 좋습니다.
다음 예에서는 thiserror
크레이트를 사용하여 오류 유형을 만듭니다.
이를 통해 thiserror::Error
트레이트를 파생시켜 열거형(enum)을 오류 유형으로 변환할 수 있습니다.
자세한 내용은 관련 문서(영어)를 참조하십시오.
// 프로그램에서 발생할 수 있는 모든 오류를 나타내는 오류 유형을 만듭니다#[derive(Debug, thiserror::Error)]enum Error { #[error(transparent)] Io(#[from] std::io::Error)}
// "serde::Serialize"를 수동으로 구현해야 합니다impl serde::Serialize for Error { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { serializer.serialize_str(self.to_string().as_ref()) }}
#[tauri::command]fn my_custom_command() -> Result<(), Error> { // 이제 오류가 반환됩니다 std::fs::File::open("path/that/does/not/exist")?; // 성공하면 `null`을 반환합니다 Ok(())}
사용자 지정 오류 유형은 발생할 수 있는 모든 오류를 명시적으로 나타내는 이점이 있어 어떤 오류가 발생할 수 있는지 즉시 식별할 수 있습니다.
이를 통해 나중에 코드를 검토하거나 리팩토링할 때 다른 사람(및 자신)의 시간을 크게 절약할 수 있습니다.
또한 오류 유형이 직렬화되는 방식을 완전히 관리할 수도 있습니다.
위의 예에서는 오류 메시지를 문자열로만 반환했지만, 각 오류에 코드를 할당하여 매우 유사한 모양의 “TypeScript 오류 열거형”에 쉽게 매핑할 수 있습니다. 예를 들어:
#[derive(Debug, thiserror::Error)]enum Error { #[error(transparent)] Io(#[from] std::io::Error), #[error("failed to parse as string: {0}")] Utf8(#[from] std::str::Utf8Error),}
#[derive(serde::Serialize)]#[serde(tag = "kind", content = "message")]#[serde(rename_all = "camelCase")]enum ErrorKind { Io(String), Utf8(String),}
impl serde::Serialize for Error { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { let error_message = self.to_string(); let error_kind = match self { Self::Io(_) => ErrorKind::Io(error_message), Self::Utf8(_) => ErrorKind::Utf8(error_message), }; error_kind.serialize(serializer) }}
#[tauri::command]fn read() -> Result<Vec<u8>, Error> { let data = std::fs::read("/path/to/file")?; Ok(data)}
프론트엔드에서는 이제 { kind: 'io' | 'utf8', message: string }
오류 객체가 표시됩니다:
type ErrorKind = { kind: 'io' | 'utf8'; message: string;};
invoke('read').catch((e: ErrorKind) => {});
Tauri에서는 UI가 멈추거나 속도가 느려지는 것을 방지하기 위해 부하가 큰 작업을 실행할 때 “비동기 명령”을 사용하는 것이 좋습니다.
명령을 비동기적으로 실행해야 하는 경우, 해당 명령을 async
로 선언하기만 하면 됩니다.
“빌린 형식”을 사용하는 경우 추가 변경을 해야 합니다. 주요 옵션은 다음 두 가지입니다:
옵션 1: &str
과 같은 형식을 String
과 같은 빌리지 않은 유사한 형식으로 변환합니다.
이 방법은 State<'_, Data>
와 같은 모든 형식에서 작동하지 않을 수 있습니다.
예:
// "&str" 대신 "String"을 사용하여 비동기 함수를 선언합니다("&str"은 빌려왔기 때문에 지원되지 않습니다)#[tauri::command]async fn my_custom_command(value: String) -> String { // 다른 비동기 함수를 호출하고 그 처리가 완료될 때까지 기다립니다 some_async_function().await; value}
옵션 2: 반환 값 형식을 Result
로 래핑합니다. 이 방법은 구현이 조금 더 어렵지만 모든 형식에서 작동합니다.
반환 값 형식 Result<a, b>
를 사용하고, a
를 “반환하려는 형식”으로 바꿉니다. null
을 반환하려면 ()
로 바꿉니다. 또한 b
를 문제가 발생했을 때 반환할 “오류 형식”으로 바꿉니다. 선택적 오류를 반환하지 않으려면 ()
로 바꿉니다. 예를 들어:
Result<String, ()>
“String 형식”을 반환하고 오류는 반환하지 않습니다.Result<(), ()>
“null
”을 반환합니다.Result<bool, Error>
위의 오류 처리 항목에서처럼 “부울 값” 또는 “오류”를 반환합니다.
예:
// 형식 빌림 문제를 피하기 위해 "Result<String, ()>"를 반환합니다#[tauri::command]async fn my_custom_command(value: &str) -> Result<String, ()> { // 다른 비동기 함수를 호출하고 그 처리가 완료될 때까지 기다립니다 some_async_function().await; // 반환 값은 "`Ok()`"로 래핑해야 한다는 점에 유의하십시오 Ok(format!(value))}
JavaScript에서의 명령 호출은 “프로미스”를 반환하므로 다른 명령과 동일하게 작동합니다:
invoke('my_custom_command', { value: 'Hello, Async!' }).then(() => console.log('Completed!'));
“Tauri 채널”은 프론트엔드로의 스트리밍 HTTP 응답과 같은 스트리밍 데이터에 대한 권장 메커니즘입니다. 다음 예에서는 파일을 읽고 프론트엔드에 4096바이트 청크(데이터 분할 단위)로 진행 상황을 알립니다:
use tokio::io::AsyncReadExt;
#[tauri::command]async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) { // 설명 간결화를 위해 이 예에서는 오류 처리가 포함되어 있지 않습니다 let mut file = tokio::fs::File::open(path).await.unwrap();
let mut chunk = vec![0; 4096];
loop { let len = file.read(&mut chunk).await.unwrap(); if len == 0 { // "길이=0"은 "파일의 끝"을 의미합니다 break; } reader.send(&chunk).unwrap(); }}
자세한 내용은 “Rust에서 프론트엔드 호출하기”의 채널 설명을 참조하십시오.
명령은 메시지를 호출한 WebviewWindow
의 인스턴스에 액세스할 수 있습니다:
#[tauri::command]async fn my_custom_command(webview_window: tauri::WebviewWindow) { println!("WebviewWindow: {}", webview_window.label());}
명령은 AppHandle
의 인스턴스에 액세스할 수 있습니다:
#[tauri::command]async fn my_custom_command(app_handle: tauri::AppHandle) { let app_dir = app_handle.path_resolver().app_dir(); use tauri::GlobalShortcutManager; app_handle.global_shortcut_manager().register("CTRL + U", move || {});}
Tauri는 tauri::Builder
의 manage
함수를 사용하여 “상태 state”를 관리할 수 있습니다.
“상태 state”에는 tauri::State
를 사용한 명령으로 액세스할 수 있습니다:
struct MyState(String);
#[tauri::command]fn my_custom_command(state: tauri::State<MyState>) { assert_eq!(state.0 == "some state value", true);}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .manage(MyState("some state value".into())) .invoke_handler(tauri::generate_handler![my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
Tauri 명령에서는 바디 페이로드(헤더 부분을 제외한 본문 부분)의 원시 데이터와 요청 헤더를 포함하는 tauri::ipc::Request
객체 전체에도 액세스할 수 있습니다.
#[derive(Debug, thiserror::Error)]enum Error { #[error("unexpected request body")] RequestBodyMustBeRaw, #[error("missing `{0}` header")] MissingHeader(&'static str),}
impl serde::Serialize for Error { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { serializer.serialize_str(self.to_string().as_ref()) }}
#[tauri::command]fn upload(request: tauri::ipc::Request) -> Result<(), Error> { let tauri::ipc::InvokeBody::Raw(upload_data) = request.body() else { return Err(Error::RequestBodyMustBeRaw); }; let Some(authorization_header) = request.headers().get("Authorization") else { return Err(Error::MissingHeader("Authorization")); };
// 업로드 중 ...
Ok(())}
프론트엔드에서는 페이로드 인수에 ArrayBuffer 또는 Uint8Array를 지정하여 Raw Request 본문을 보내는 “invoke()“를 호출할 수 있습니다. 세 번째 인수에 요청 헤더를 포함할 수도 있습니다:
const data = new Uint8Array([1, 2, 3]);await __TAURI__.core.invoke('upload', data, { headers: { Authorization: 'apikey', },});
“tauri::generate_handler!
매크로”는 명령 배열을 받습니다. 여러 명령을 등록하는 경우 invoke_handler를 여러 번 호출할 수 없습니다. 마지막 호출만 사용됩니다. 각 명령을 하나씩 tauri::generate_handler!
호출에 전달해야 합니다.
#[tauri::command]fn cmd_a() -> String { "Command a"}#[tauri::command]fn cmd_b() -> String { "Command b"}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![cmd_a, cmd_b]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
위의 기능 중 일부 또는 전부를 결합할 수 있습니다:
struct Database;
#[derive(serde::Serialize)]struct CustomResponse { message: String, other_val: usize,}
async fn some_other_function() -> Option<String> { Some("response".into())}
#[tauri::command]async fn my_custom_command( window: tauri::Window, number: usize, database: tauri::State<'_, Database>,) -> Result<CustomResponse, String> { println!("Called from {}", window.label()); let result: Option<String> = some_other_function().await; if let Some(message) = result { Ok(CustomResponse { message, other_val: 42 + number, }) } else { Err("No result".into()) }}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .manage(Database {}) .invoke_handler(tauri::generate_handler![my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
import { invoke } from '@tauri-apps/api/core';
// Invocation from JavaScriptinvoke('my_custom_command', { number: 42,}) .then((res) => console.log(`Message: ${res.message}, Other Val: ${res.other_val}`) ) .catch((e) => console.error(e));
“이벤트 시스템”은 프론트엔드와 Rust 간의 더 간결한 통신 메커니즘입니다. 명령과 달리 이벤트는 형식 안전하지 않으며, 항상 비동기적이며, 값을 반환할 수 없고, JSON 페이로드만 지원합니다.
전역 이벤트를 트리거하려면 event.emit 또는 WebviewWindow#emit 함수를 사용할 수 있습니다:
import { emit } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emit(eventName, payload)emit('file-selected', '/path/to/file');
const appWebview = getCurrentWebviewWindow();appWebview.emit('route-changed', { url: window.location.href });
개별 Webview에 의해 등록된 리스너에 대해 이벤트를 트리거하려면 event.emitTo 또는 WebviewWindow#emitTo 함수를 사용할 수 있습니다:
import { emitTo } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emitTo(webviewLabel, eventName, payload)emitTo('settings', 'settings-update-requested', { key: 'notification', value: 'all',});
const appWebview = getCurrentWebviewWindow();appWebview.emitTo('editor', 'file-changed', { path: '/path/to/file', contents: 'file contents',});
@tauri-apps/api
NPM 패키지는 전역 이벤트와 Webview 특정 이벤트를 모두 감지(수신)하기 위한 API를 제공합니다.
-
전역 이벤트 감지
import { listen } from '@tauri-apps/api/event';type DownloadStarted = {url: string;downloadId: number;contentLength: number;};listen<DownloadStarted>('download-started', (event) => {console.log(`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`);}); -
Webview 특정 이벤트 감지
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';const appWebview = getCurrentWebviewWindow();appWebview.listen<string>('logged-in', (event) => {localStorage.setItem('session-token', event.payload);});
The listen
함수는 애플리케이션의 전체 “라이프타임” 기간 동안 이벤트 리스너 등록이 유지됩니다.
이벤트 감지(수신)를 중지하려면 listen
함수가 반환하는 unlisten
함수를 사용할 수 있습니다:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});unlisten();
전역 이벤트와 Webview 특정 이벤트 모두 Rust에 등록된 리스너에게 전달됩니다.
-
전역 이벤트 감지
src-tauri/src/lib.rs use tauri::Listener;#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {tauri::Builder::default().setup(|app| {app.listen("download-started", |event| {if let Ok(payload) = serde_json::from_str::<DownloadStarted>(&event.payload()) {println!("downloading {}", payload.url);}});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");} -
Webview 특정 이벤트 감지
src-tauri/src/lib.rs use tauri::{Listener, Manager};#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {tauri::Builder::default().setup(|app| {let webview = app.get_webview_window("main").unwrap();webview.listen("logged-in", |event| {let session_token = event.data;// save token..});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");}
The listen
함수는 애플리케이션의 전체 “라이프타임” 기간 동안 이벤트 리스너 등록이 유지됩니다.
이벤트 감지(수신)를 중지하려면 listen
함수가 반환하는 unlisten
함수를 사용할 수 있습니다:
// unlisten outside of the event handler scope:let event_id = app.listen("download-started", |event| {});app.unlisten(event_id);
// unlisten when some event criteria is matchedlet handle = app.handle().clone();app.listen("status-changed", |event| { if event.data == "ready" { handle.unlisten(event.id); }});
또한 Tauri는 이벤트를 한 번만 감지(수신)하기 위한 유틸리티 함수를 제공합니다:
app.once("ready", |event| { println!("app is ready");});
이 경우 이벤트 리스너는 첫 번째 트리거 후 즉시 등록 해제됩니다.
Rust 코드에서 이벤트를 감지(수신)하거나 이벤트를 발행하는 방법에 대해서는 이전 장 “Rust에서 프론트엔드 호출하기”의 이벤트 시스템 설명을 참조하십시오.
© 2025 Tauri Contributors. CC-BY / MIT