rust wasm 삽질. 웹통신
sveltekit PWA 와 rust WASM 으로 코인 생태계 시뮬레이션 만들고 있는데요
(게임엔진 macroquad, 물리엔진 rapier)
너무 삽질을 많이 했네요
백엔드 서버에서 rust was로 바이낸스 api에 1초 단위로 BTC 가격 가져와서 redis에 저장한 뒤
클라이언트 웹브라우저(wasm pwa)가 15초에 한번씩 api 요청하면 redis streams로 queue 순서대로 데이터 보내주는건데요
언뜻 생각하기에는 rust wasm 내부에서 게임 시뮬레이션 랜더링도 다 하고 백엔드서버와 통신도 다해서 막바로 데이터를 사용하는게 좋을것 같아서 tokio async로 api 요청을 보낼려고 했는데
결국 포기했네요
그냥 js fetch, ajax 로 api 요청하는것이 v8 engine 구조상 더 웹 io에 따로 자원을 배분하기 때문에 tokio asnyc보다 더 효율적이라네요
tokio async는 어차피 v8 engine을 통해 빌려쓰는 싱글쓰레드로 io를 처리하는데 이렇게 되면 2번 둘러간다는거죠
tokio async는 os단에서는 직접적인 자원활용으로 최강의 멀티쓰레드 성능이 나오는건데 웹 wasm에서는 너무 복잡하고 쓸데가 없네요
그러니까 남들이 하는건 다 이유가 있다는거죠. wasm 엔진은 계산만 하고 랜더링만 해라. pwa한테 ui와 웹통신 시켜라
이걸 모르고 아무리 해도 컴파일 에러, 런타임 에러 계속 나는데 하루 다 썼네요
Rust wasm macroquad와 sveltekit pwa 게임에서 왜 tokio async로 백엔드서버의 데이터를 가져오는 것보다 js가 웹 입출력 처리 해서 rust wasm 으로 넘겨주는게 좋다고 하는지 알려줘
Rust Wasm과 JavaScript의 역할 분담: 왜 직접적인 백엔드 통신보다 JS를 통한 데이터 전달이 선호되는가?
Rust Wasm(WebAssembly)과 JavaScript(JS)는 웹 애플리케이션에서 상호 보완적인 역할을 수행합니다. 질문에서 언급하신 것처럼, Rust Wasm에서 직접 백엔드 서버에 데이터를 요청하는 것보다 JavaScript를 통해 데이터를 가져와 Rust Wasm으로 전달하는 방식이 선호되는 이유를 설명해드리겠습니다.
1. 웹 입출력(I/O)의 본질
* 브라우저의 I/O 처리 모델: 브라우저는 JavaScript의 fetch API, XMLHttpRequest와 같은 비동기 I/O 함수를 통해 네트워크 통신을 처리하도록 설계되었습니다. 이러한 API들은 브라우저의 이벤트 루프와 통합되어 있어, I/O 작업이 완료될 때까지 애플리케이션이 멈추지 않고 다른 작업을 수행할 수 있도록 해줍니다.
* Wasm의 한계: Wasm은 기본적으로 샌드박스 환경에서 실행되는 저수준의 바이너리 코드입니다. Wasm 자체에는 네트워크 소켓을 직접 열거나 HTTP 요청을 보내는 기능이 내장되어 있지 않습니다. Wasm 모듈은 브라우저의 Web API에 접근하기 위해 JavaScript를 통한 "호스트 함수(Host Function)" 호출 메커니즘을 사용해야 합니다.
2. tokio와 같은 런타임의 복잡성
* tokio의 역할: tokio는 Rust의 비동기 런타임으로, OS의 비동기 I/O를 효율적으로 처리하기 위해 설계되었습니다. 이는 주로 서버 측 애플리케이션에서 사용되며, TCP/UDP 소켓과 같은 저수준 네트워크 I/O를 다룹니다.
* 브라우저 환경과의 불일치: tokio와 같은 런타임은 브라우저 환경에서 직접 실행될 수 없습니다. 브라우저는 OS의 소켓을 직접 다루는 것을 허용하지 않으며, JavaScript의 fetch API와 같은 고수준의 Web API를 사용해야 합니다.
* 억지로 통합하려 할 때의 복잡성: tokio를 브라우저에서 사용하려면, tokio의 I/O 스케줄러를 브라우저의 이벤트 루프와 연동시키는 복잡한 추상화 레이어가 필요합니다. 이는 Rust Wasm 모듈의 크기를 불필요하게 키우고, 의존성을 추가하며, 디버깅을 어렵게 만듭니다. 또한, 이미 브라우저에 최적화된 I/O 처리 기능이 있는데, 굳이 Rust에서 직접 I/O를 구현하는 것은 비효율적입니다.
3. JavaScript를 통한 데이터 전달이 효율적인 이유
* 기존 인프라 활용: JavaScript는 브라우저의 네트워크 스택을 가장 잘 활용할 수 있도록 설계되었습니다. fetch API는 CORS(Cross-Origin Resource Sharing) 정책, 캐싱, 인증 등 웹 표준을 준수하며 백엔드 통신을 안전하고 효율적으로 처리합니다.
* 직관적인 역할 분담:
* JavaScript: 웹 I/O(네트워크 요청, DOM 조작 등)를 담당하는 "프런트엔드 컨트롤러" 역할을 수행합니다.
* Rust Wasm: 순수하고 복잡한 계산, 게임 로직, 물리 엔진, 데이터 처리 등 CPU 집약적인 작업을 효율적으로 수행하는 "컴퓨팅 엔진" 역할을 담당합니다.
* 개발 편의성:
* 생태계 활용: JavaScript의 풍부한 웹 라이브러리(Axios, Superagent 등)를 활용하여 API 통신을 더 쉽게 구현할 수 있습니다.
* 디버깅 용이성: 네트워크 요청은 브라우저의 개발자 도구(Network 탭)에서 쉽게 모니터링하고 디버깅할 수 있습니다. Rust Wasm 내부에서 발생한 네트워크 관련 문제를 디버깅하는 것보다 훨씬 간단합니다.
* 번들 크기 최소화: Rust Wasm 모듈은 필요한 로직만 담고 있어 번들 크기를 작게 유지할 수 있습니다. reqwest와 같은 HTTP 클라이언트 라이브러리를 추가하면 번들 크기가 크게 늘어날 수 있습니다.
결론
Rust Wasm과 JavaScript의 협업에서, 각 언어의 강점을 최대한 활용하는 것이 가장 효율적입니다. JavaScript는 웹의 "팔다리"로서 브라우저의 API를 통해 외부 세계와 소통하는 역할을 맡고, Rust Wasm은 웹의 "두뇌"로서 복잡한 로직을 빠르고 안정적으로 처리하는 역할을 맡는 것입니다.
따라서 Rust Wasm 게임에서 백엔드 데이터를 가져올 때, JavaScript의 fetch API를 통해 데이터를 가져온 후, 그 결과를 Rust Wasm 모듈로 전달하여 처리하는 방식이 더 자연스럽고, 효율적이며, 유지보수가 용이한 아키텍처입니다.
이러한 역할 분담은 wasm-bindgen과 같은 도구를 통해 Rust와 JavaScript 간의 데이터 직렬화 및 역직렬화를 효율적으로 처리할 수 있어 더욱 강력해집니다.