"다리 위에서 양쪽 말이 맞는지 검사해주는 컴파일러는 없어. 그러니 그 보장은 네가 직접 만들거나, 아예 기계한테 만들게 시켜."
아무도 안 메워주는 틈
invoke<Note>('get_note', ...)라고 쓸 때 Note는 확인된 사실이 아니라 네 주장이야. Rust랑 TypeScript는 따로 컴파일되거든. Rust command가 정말로 네 TS 인터페이스랑 같은 모양을 돌려주는지 검사하는 건 아무것도 없어. Rust에서 필드 이름 하나를 바꿔도 네 TS는 런타임에 깨질 때까지 거짓말 위에서 멀쩡히 컴파일돼. 잘 만든 Tauri 앱은 이 틈을 작정하고 메워.
전략 1: 타입 붙은 API 모듈 하나
최소한의 규율은 이거야. 컴포넌트에선 invoke를 생으로 절대 안 불러. 대신 command를 전부 타입 함수로 감싼 모듈 하나를 둬 — getNote(id: number): Promise<Note> 같은 식으로. 그럼 command 이름이랑 인자 모양이 정확히 한 곳에만 살아. command가 바뀌면 파일 하나만 고치고, 나머지 부르는 곳은 TypeScript가 다 짚어줘. fetch 호출을 API 레이어로 감싸던 그 본능을 IPC에 그대로 옮긴 거야.
전략 2: 타입을 생성해
더 튼튼한 선택지는 Rust command에서 TypeScript 바인딩을 뽑아내서 양쪽이 어긋날 수 없게 만드는 거야. tauri-specta 같은 도구가 네 command 시그니처를 읽고 타입이 붙은 클라이언트 함수를 내놔 — Rust에서 필드 이름을 바꾸면 생성된 TS가 따라 바뀌면서 런타임이 아니라 컴파일 타임에 빌드를 깨뜨려. 진지한 앱이면 세팅할 값이 있어. 아무도 안 메워주던 틈을 컴파일러가 지키는 계약으로 바꿔놓거든.
Rust는 여기서 어디로
여기까지가 다리야. 딱 필요한 만큼의 Rust, command, serde, async, 에러, 타입. 진짜 앱을 만들기엔 충분해. 언어를 더 깊이 파고 싶어질 때 — trait, lifetime, 제네릭, iterator 도구상자, 겁 없는 동시성 — 그건 /cwk-quests/rust-quest에 있는 자기 퀘스트야. 넌 다리를 건넜어. 이 퀘스트의 나머지는 건너편에서 지어져.