ABOUT ME

-

Total
-
  • Rust: WASM async fn + 카카오맵 API 사용하기
    컴퓨터/Rust 2023. 6. 26. 12:05
    728x90
    반응형

    소개

    이 글에선 위와 같은 웹을 만들어 볼 것이다.

    카카오맵 키워드 검색 함수를 Rust로 만들고 WASM으로 변환한다.

    Rust WASM으로 유명한 wasm-bindgen 라이브러리는 Rust와 JavaScript 간의 상호 운용성을 향상해,

    Rust에서 작성된 코드를 WASM으로 컴파일하고 JavaScript에서 이를 호출하게 할 수 있다.

     

    Rust에서 WASM으로 컴파일하는 기본 단계는 다음과 같다:

    • Rust에서 원하는 기능(여기서는 카카오맵 키워드 검색과 같은 것)을 구현
    • wasm-bindgen을 사용하여 WASM과 JavaScript 간의 상호 운용성을 설정
    • wasm-pack 또는 비슷한 도구를 사용하여 Rust 코드를 WASM으로 컴파일
    • 컴파일된 WASM 코드를 웹 페이지에 로드하고 JavaScript를 사용하여 호출

     

    준비

     

    우선 Rust 프로젝트를 손쉽게 WASM으로 변환하기 위해 툴을 설치한다.

    cargo install wasm-pack

     

    @카카오 디벨로퍼에서 앱을 만들고 JavaScript 키 (HTML에서 맵 그리기)와 REST API 키 (키워드 검색 호출)를 복사해 둔다. (API 키는 어차피 `플랫폼 - Web`에 추가된 주소에만 가능해서 노출돼도 크게 상관은 없다.)

     

    @카카오맵 Web API를 들어가서 Guide를 훑어보면서 어떻게 쓰는지 본다. (1일 300,000회 무료 이용)

     

    WASM

     

    Cargo.toml

    [lib] 블록: 라이브러리의 설정을 선언하는 섹션이다. 여기서는 crate-type이 cdylib로 설정되어 있으며, 이는 Rust가 C 호환 동적 라이브러리 (.so, .dylib 또는 .dll)를 생성하도록 지시하는 설정이다. 이러한 라이브러리 형태는 WebAssembly와 함께 사용할 때 필요하다.

    각 라이브러리들을 설명하면 (필요에 맞게 알아서 빼거나 추가한다. 여기에서는 async 함수를 만들 것이다.)

    • js-sys: Rust에서 순수 JavaScript 함수와 객체를 제공하는 라이브러리입니다.
    • reqwest: Rust로 작성된 인기 있는 HTTP 클라이언트 라이브러리입니다. 이 라이브러리는 비동기 I/O를 지원하며, json 기능은 JSON 처리 기능을 활성화합니다.
    • serde: Rust의 데이터 직렬화/역직렬화 라이브러리입니다. derive 기능은 자동으로 데이터 타입에 대한 직렬화/역직렬화 구현을 생성하는 데 사용됩니다.
    • serde_json: JSON 데이터를 처리하는 serde의 확장 라이브러리입니다.
    • wasm-bindgen: Rust와 JavaScript 간의 상호 운용성을 가능하게 하는 라이브러리입니다. 이 라이브러리를 통해 Rust에서 작성된 함수를 JavaScript에서 직접 호출하거나, 반대로 JavaScript 함수를 Rust에서 호출할 수 있습니다.
    • wasm-bindgen-futures: JavaScript의 Promise와 Rust의 Future 사이의 변환을 도와주는 라이브러리입니다.
    • web-sys: 웹 API에 대한 Rust 바인딩을 제공하는 라이브러리입니다. 여기에는 Headers, Request, RequestInit, RequestMode, Response, Window 등의 특징을 활성화합니다.
    • serde-wasm-bindgen: serde와 wasm-bindgen을 함께 사용하기 위한 라이브러리입니다. 이 라이브러리를 사용하면 JavaScript 객체를 Rust 객체로, 또는 그 반대로 변환할 수 있습니다.

     

    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    js-sys = "0.3"
    reqwest = { version = "0.11", features = ["json"] }
    serde = { version = "1", features = ["derive"] }
    serde_json = "1.0"
    wasm-bindgen = "0.2"
    wasm-bindgen-futures = "0.4"
    web-sys = { version = "0.3",features = [
      'Headers',
      'Request',
      'RequestInit',
      'RequestMode',
      'Response',
      'Window',
    ] }
    serde-wasm-bindgen = "0.5"

     

    src/lib.rs

    주요 작업 흐름은 다음과 같다:

    • 먼저, "Place"라는 구조체를 정의하여 장소 정보를 저장합니다. 이 구조체는 각 장소의 주소, 이름, 그리고 좌표를 가지고 있습니다. (원하는 자료는 알아서 추가)
    • 그다음으로, 비동기 fetch 함수를 선언하여 JavaScript의 window.fetch 함수를 호출할 수 있도록 합니다. 이 함수는 주어진 URL로 HTTP 요청을 보내고 그 결과를 비동기적으로 반환합니다.
    • get_places 함수를 정의합니다. 이 함수는 검색 문자열을 입력으로 받아 카카오 키워드 장소 검색 API를 호출합니다. 이 함수는 fetch 함수를 사용하여 HTTP GET 요청을 보내고, Authorization 헤더에 API 키를 포함시킵니다.
    • HTTP 요청의 결과를 받으면, 이를 JSON 형태로 파싱 합니다. 이때, 파싱 된 JSON 데이터는 Body 구조체로 변환되며, 이 구조체는 검색된 장소의 정보를 가지는 Place 구조체의 벡터를 가지고 있습니다.
    • 마지막으로, Body 구조체를 JavaScript 객체로 다시 변환하고 이를 반환합니다. 이렇게 변환된 객체는 JavaScript 코드에서 사용할 수 있습니다.

    이 코드를 통해, Rust에서 카카오 키워드 장소 검색 API를 호출하고 그 결과를 JavaScript에서 사용할 수 있는 형태로 변환하는 WebAssembly 기능을 구현할 수 있습니다. (REST API 키를 복붙 할 것)

     

    인터페이스 export는 extern "C", 함수를 만들 땐 #[wasm_bindgen] proc을 붙여 넣는다.

    JsFuture, JsCast, JsValue 등은 마지막 참고 링크들로 공부한다.

    use serde::{Deserialize, Serialize};
    use wasm_bindgen::prelude::*;
    use wasm_bindgen::JsCast;
    use wasm_bindgen_futures::JsFuture;
    use web_sys::{Headers, Request, RequestInit, Response};
    
    // 장소 정보를 담기 위한 구조체
    #[derive(Serialize, Deserialize)]
    struct Place {
        address_name: String,
        place_name: String,
        x: String,
        y: String,
    }
    
    // 외부 JavaScript fetch 함수와의 인터페이스 (비동기임)
    #[wasm_bindgen]
    extern "C" {
        #[wasm_bindgen(js_namespace = window, catch)]
        async fn fetch(url: &str, headers: JsValue) -> Result<JsValue, JsValue>;
    }
    
    // 장소 검색 함수
    #[wasm_bindgen]
    pub async fn get_places(search_string: &str) -> Result<JsValue, JsValue> {
        // 검색 쿼리를 사용해 URL 생성
        let url = format!(
            "https://dapi.kakao.com/v2/local/search/keyword.json?query={}",
            search_string
        );
    
        let mut opts = RequestInit::new();
        opts.method("GET");
    
        let headers = Headers::new().unwrap();
        // Kakao API를 사용하기 위해 필요한 Authorization 헤더 설정
        headers
            .set("Authorization", "KakaoAK REST_API_KEY")
            .unwrap();
    
        opts.headers(&headers);
    
        let request = Request::new_with_str_and_init(&url, &opts).unwrap();
    
        let window = web_sys::window().unwrap();
        // fetch 요청 보내기
        let resp_value = JsFuture::from(window.fetch_with_request(&request))
            .await
            .map_err(|_| "Failed to fetch data")?;
    
        let resp: Response = resp_value.dyn_into().unwrap();
    
        let json_value = JsFuture::from(resp.json().unwrap()).await.unwrap();
    
        // 응답 바디를 자료형으로 변환
        #[derive(Deserialize)]
        struct Body {
            documents: Vec<Place>,
        }
    
        let body: Body = serde_wasm_bindgen::from_value(json_value.clone())
            .map_err(|_| "Failed to parse response data")?;
    
        // 변환된 자료형에서 장소 데이터 추출
        let places_js: JsValue = serde_wasm_bindgen::to_value(&body.documents)
            .map_err(|_| "Failed to convert data to JS value")?;
    
        Ok(places_js)
    }

     

    컴파일

    script type wasm을 모든 웹이 지원하는 것이 아니라서 target을 지정해서 컴파일해줘야 한다.

    wasm-pack build --release --target web

     

    컴파일하면 pkg 폴더 안에 wasm이랑 js 파일들이 생성된다.

     

    wasm.html

    여기에선 카카오맵 SDK (JavaScript 키)와 Marker를 이용해서 지도에 표시할 것이다.

    마커 (Marker)

     

    아래 코드에서 input에 키워드를 입력하게 하고 엔터를 누를 때마다 지도를 렌더링 해서 보여준다.

    async function run() 부분이 WASM의 핵심이다. init을 부르고 자신이 만든 함수를 불러오면 된다.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>카카오맵 WASM</title>
        <meta name="viewport" content="width=device-width,initial-scale=1" />
    
        <style>
          /* Add styles for header, logo and map */
          #header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px;
            background-color: #f5f5f5;
          }
          #logo {
            font-size: 24px;
            font-weight: bold;
          }
          #map {
            position: absolute;
            top: 50px; /* Height of the header */
            left: 0;
            right: 0;
            bottom: 0;
          }
        </style>
        <script
          type="text/javascript"
          src="//dapi.kakao.com/v2/maps/sdk.js?appkey=JS_API_KEY"
        ></script>
      </head>
      <body>
        <!-- simple header and input -->
        <div id="header">
          <div id="logo">TITLE</div>
          <input type="text" id="search-input" placeholder="Search..." />
        </div>
        <!-- The map container -->
        <div id="map"></div>
        <script type="module">
          import init, { get_places } from "./pkg/내 러스트 pkg.js";
    
          async function run() {
            await init(); // WASM 모듈 초기화
    
            const input = document.getElementById("search-input");
            input.addEventListener("keydown", async function (event) {
              if (event.key === "Enter") {
                let places = await get_places(event.target.value);
                renderMap(places);
              }
            });
          }
    
          function renderMap(places) {
            const mapContainer = document.getElementById("map");
            const mapOption = {
              center: new kakao.maps.LatLng(places[0].y, places[0].x),
              level: 4,
              mapTypeId: kakao.maps.MapTypeId.ROADMAP,
            };
    
            // Create the map
            const map = new kakao.maps.Map(mapContainer, mapOption);
    
            // Function to display marker
            function displayMarker(locPosition, message) {
              const marker = new kakao.maps.Marker({
                map: map,
                position: locPosition,
              });
    
              const infowindow = new kakao.maps.InfoWindow({
                content: message,
                removable: false,
              });
    
              infowindow.open(map, marker);
              map.setCenter(locPosition);
            }
    
            // Loop through each place and display marker
            places.forEach(place => {
              const locPosition = new kakao.maps.LatLng(place.y, place.x);
              const message = `<div style="padding:5px;">${place.place_name}<br/>${place.address_name}</div>`;
              displayMarker(locPosition, message);
            });
          }
    
          run();
        </script>
      </body>
    </html>

     

    참고

    https://rustwasm.github.io/wasm-bindgen/

     

    Introduction - The wasm-bindgen Guide

    This book is about wasm-bindgen, a Rust library and CLI tool that facilitate high-level interactions between wasm modules and JavaScript. The wasm-bindgen tool and crate are only one part of the Rust and WebAssembly ecosystem. If you're not familiar alread

    rustwasm.github.io

    https://rustwasm.github.io/wasm-bindgen/api/web_sys/

     

    web_sys - Rust

     

    rustwasm.github.io

    https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/

     

    wasm_bindgen_futures - Rust

    Converting between JavaScript Promises to Rust Futures. This crate provides a bridge for working with JavaScript Promise types as a Rust Future, and similarly contains utilities to turn a rust Future into a JavaScript Promise. This can be useful when worki

    rustwasm.github.io

     

    728x90

    '컴퓨터 > Rust' 카테고리의 다른 글

    Rust: 카카오 소셜 로그인 하기 (JWT, actix-rs, react.js)  (0) 2023.07.10
    Rust: actix-rs + React.js  (0) 2023.06.02
    Rust 문법: Box  (0) 2023.05.13

    댓글