"느린 동기 command는 앱을 얼려버려. async는 작업이 도는 동안에도 창이 계속 숨 쉬게 해줘."
async만 붙이면 돼
command를 비동기로 만들려면 async fn으로 쓰면 끝이야. Tauri가 그걸 async 런타임(Tokio)에서 돌리고, 프론트엔드의 invoke Promise는 future가 끝날 때 resolve돼. 이게 중요한 이유는, 오래 걸리는 동기 command가 스레드를 붙잡고 있으면 UI가 얼어붙은 것처럼 보이기 때문이야. 네트워크 호출이든, 자식 프로세스를 기다리는 일이든, I/O로 시간을 보내는 일이든 — async로 만들면 앱이 계속 반응해.
I/O 바운드랑 CPU 바운드
여기 진짜 갈림길이 있어. 기다리는 게 일인 I/O 바운드 작업(HTTP, 디스크, 대기)은 async랑 딱 맞아 — 런타임이 기다리는 태스크를 잠깐 세워두고 다른 걸 돌리거든. 계산이 일인 CPU 바운드 작업(거대한 이미지 리사이즈, 해싱, 무거운 파싱)은 async만으론 얻는 게 없어. async 태스크 안에서 CPU를 계속 돌리면 런타임 자체가 멈춰 설 수 있어. 무거운 계산은 tokio::task::spawn_blocking(아니면 그냥 std::thread)으로 블로킹 스레드에 넘겨서 async 런타임을 풀어줘.
Send 함정
컴파일러가 강제하는 규칙이 하나 있어. async command에서 await를 넘어가며 붙잡고 있는 데이터는 스레드 사이를 옮겨도 안전(Send)해야 해. 여기서 제일 흔히 걸리는 게 std::sync::Mutex 가드를 await 지점 너머까지 쥐고 있는 거야 — 컴파일이 안 돼. 해결책은 await 전에 락을 놓거나 async를 아는 락을 쓰는 거고. state 트랙에서 제대로 만날 테니 지금은 이것만 기억해. 'cannot be sent between threads safely'는 보통 'await를 넘어가며 락을 쥐고 있었다'는 뜻이야.