Skip to content

UI 연결

가장 작은 브라우저 흐름부터 시작한 뒤, 사용하는 UI 프레임워크에 맞게 확장하세요.

최소 예제

이 예제는 버튼 하나로 인식을 시작하고, 인식된 campaign name을 콘솔에 출력합니다.

html
<button id="start-button" type="button">Start Recognition</button>

<script type="module">
  import { AudioRecognizer } from "@sorisdk/web-audio";

  const startButton = document.querySelector("#start-button");

  const recognizer = new AudioRecognizer({
    appId: "YOUR_APP_ID",
    ephemeralKey: async () => {
      const response = await fetch("/api/ephemeral-key", {
        method: "POST"
      });

      if (!response.ok) {
        throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
      }

      const { ephemeral_key } = await response.json();
      return ephemeral_key;
    }
  });

  recognizer.on("campaign", (event) => {
    console.log(event.campaign.name);
  });

  startButton?.addEventListener("click", async () => {
    await recognizer.start();
  });
</script>

이 예제는 이미 ephemeral key endpoint가 준비되어 있다고 가정합니다. 표준 설정은 Ephemeral Key에서 확인하세요.

고급 예제

아래 예제들은 인식된 campaign을 UI에 렌더링합니다. React, Vue 등 Vite 기반 프레임워크를 사용한다면 먼저 설정 문서의 wasm 로더 구성을 적용하세요.

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SORI Web SDK</title>
    <style>
      #campaign-list {
        padding: 0;
        list-style: none;
      }

      #campaign-list li + li {
        margin-top: 8px;
      }
    </style>
  </head>
  <body>
    <main>
      <button id="start-button" type="button">Start Recognition</button>
      <ul id="campaign-list"></ul>
    </main>

    <script type="module">
      import { AudioRecognizer } from "@sorisdk/web-audio";

      const startButton = document.querySelector("#start-button");
      const campaignList = document.querySelector("#campaign-list");

      const recognizer = new AudioRecognizer({
        appId: "YOUR_APP_ID",
        ephemeralKey: async () => {
          const response = await fetch("/api/ephemeral-key", {
            method: "POST"
          });

          if (!response.ok) {
            throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
          }

          const { ephemeral_key } = await response.json();
          return ephemeral_key;
        }
      });

      recognizer.on("campaign", (event) => {
        console.log(event.campaign.name);
        if (campaignList) {
          const item = document.createElement("li");
          const link = document.createElement("a");

          link.href = String(event.campaign.action_url);
          link.textContent = String(event.campaign.title);
          link.target = "_blank";
          link.rel = "noreferrer";

          item.append(link);
          campaignList.prepend(item);
        }
      });

      startButton?.addEventListener("click", async () => {
        await recognizer.start();
      });

      globalThis.addEventListener("beforeunload", () => {
        void recognizer.destroy();
      });
    </script>
  </body>
</html>
tsx
import { useEffect, useRef, useState } from "react";
import { AudioRecognizer } from "@sorisdk/web-audio";

type CampaignCard = {
  title: string;
  imageUrl: string;
  actionUrl: string;
};

export default function App() {
  const recognizerRef = useRef<AudioRecognizer | null>(null);
  const [campaigns, setCampaigns] = useState<CampaignCard[]>([]);

  useEffect(() => {
    const recognizer = new AudioRecognizer({
      appId: "YOUR_APP_ID",
      ephemeralKey: async () => {
        const response = await fetch("/api/ephemeral-key", {
          method: "POST"
        });

        if (!response.ok) {
          throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
        }

        const { ephemeral_key } = await response.json();
        return ephemeral_key;
      }
    });

    recognizer.on("campaign", (event) => {
      console.log(event.campaign.name);
      setCampaigns((current) => [
        {
          title: String(event.campaign.title),
          imageUrl: String(event.campaign.image_url),
          actionUrl: String(event.campaign.action_url)
        },
        ...current
      ]);
    });

    recognizerRef.current = recognizer;

    return () => {
      void recognizer.destroy();
      recognizerRef.current = null;
    };
  }, []);

  return (
    <main>
      <button
        type="button"
        onClick={() => {
          void recognizerRef.current?.start();
        }}
      >
        Start Recognition
      </button>
      <div>
        {campaigns.map((campaign, index) => (
          <a
            key={`${campaign.title}-${index}`}
            href={campaign.actionUrl}
            target="_blank"
            rel="noreferrer"
          >
            <img src={campaign.imageUrl} alt={campaign.title} />
            <strong>{campaign.title}</strong>
          </a>
        ))}
      </div>
    </main>
  );
}
vue
<script setup lang="ts">
import { onBeforeUnmount } from "vue";
import { ref } from "vue";
import { AudioRecognizer } from "@sorisdk/web-audio";

const campaigns = ref<
  Array<{ title: string; imageUrl: string; actionUrl: string }>
>([]);

const recognizer = new AudioRecognizer({
  appId: "YOUR_APP_ID",
  ephemeralKey: async () => {
    const response = await fetch("/api/ephemeral-key", {
      method: "POST"
    });

    if (!response.ok) {
      throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
    }

    const { ephemeral_key } = await response.json();
    return ephemeral_key;
  }
});

recognizer.on("campaign", (event) => {
  console.log(event.campaign.name);
  campaigns.value.unshift({
    title: String(event.campaign.title),
    imageUrl: String(event.campaign.image_url),
    actionUrl: String(event.campaign.action_url)
  });
});

async function startRecognition() {
  await recognizer.start();
}

onBeforeUnmount(() => {
  void recognizer.destroy();
});
</script>

<template>
  <main>
    <button type="button" @click="startRecognition">Start Recognition</button>
    <div class="campaign-grid">
      <a
        v-for="campaign in campaigns"
        :key="`${campaign.actionUrl}-${campaign.title}`"
        :href="campaign.actionUrl"
        target="_blank"
        rel="noreferrer"
        class="campaign-card"
      >
        <img :src="campaign.imageUrl" :alt="campaign.title" />
        <strong>{{ campaign.title }}</strong>
      </a>
    </div>
  </main>
</template>

<style scoped>
.campaign-grid {
  display: grid;
  gap: 12px;
  margin-top: 16px;
}

.campaign-card {
  display: grid;
  gap: 8px;
  color: inherit;
  text-decoration: none;
}
</style>
svelte
<script lang="ts">
  import { onMount } from "svelte";
  import { AudioRecognizer } from "@sorisdk/web-audio";

  let campaigns: Array<{ title: string; imageUrl: string; actionUrl: string }> = [];
  let recognizer: AudioRecognizer;

  onMount(() => {
    recognizer = new AudioRecognizer({
      appId: "YOUR_APP_ID",
      ephemeralKey: async () => {
        const response = await fetch("/api/ephemeral-key", {
          method: "POST"
        });

        if (!response.ok) {
          throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
        }

        const { ephemeral_key } = await response.json();
        return ephemeral_key;
      }
    });

    recognizer.on("campaign", (event) => {
      console.log(event.campaign.name);
      campaigns = [
        {
          title: String(event.campaign.title),
          imageUrl: String(event.campaign.image_url),
          actionUrl: String(event.campaign.action_url)
        },
        ...campaigns
      ];
    });

    return () => {
      void recognizer.destroy();
    };
  });

  async function startRecognition() {
    await recognizer.start();
  }
</script>

<main>
  <button type="button" on:click={startRecognition}>Start Recognition</button>
  <div class="campaign-grid">
    {#each campaigns as campaign}
      <a
        href={campaign.actionUrl}
        target="_blank"
        rel="noreferrer"
        class="campaign-card"
      >
        <img src={campaign.imageUrl} alt={campaign.title} />
        <strong>{campaign.title}</strong>
      </a>
    {/each}
  </div>
</main>

<style>
  .campaign-grid {
    display: grid;
    gap: 12px;
    margin-top: 16px;
  }

  .campaign-card {
    display: grid;
    gap: 8px;
    color: inherit;
    text-decoration: none;
  }
</style>

참고 사항

  • 마이크 권한 요청은 버튼 클릭 같은 사용자 액션에서 시작하세요.
  • 페이지나 컴포넌트가 제거될 수 있다면 cleanup 시점에 destroy()를 호출하세요.
  • 더 복잡한 제어 흐름이 필요하다면, 이 최소 연결이 동작한 뒤 stop()과 추가 이벤트 핸들러를 붙이는 편이 좋습니다.