Flutter Web에서 대화 메모리를 갖춘 음성 AI 채팅 구현 — Web Speech API + Supabase

발행: (2026년 4월 17일 AM 10:48 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

번역을 진행하려면 실제 번역이 필요한 본문 텍스트를 제공해 주시겠어요?
본문을 알려주시면 소스 링크는 그대로 유지하면서, 마크다운 형식과 코드 블록, URL, 기술 용어는 그대로 두고 나머지 내용을 한국어로 번역해 드리겠습니다.

소개

저는 개인 앱 “自分株式会社”(Jibun Kabushiki Kaisha)에 음성 기반 AI 채팅지속적인 대화 기록을 추가했습니다.

  • 마이크 입력 → 텍스트 변환을 위한 Web Speech API(브라우저 내장)
  • 대화 기록 저장을 위한 Supabase conversation_messages 테이블
  • 장기 메모리를 위해 기존 ai‑assistant Edge Function에 chat 액션을 추가

예상보다 훨씬 적은 코드만으로 구현할 수 있었습니다.

기술 스택

계층기술
프론트엔드Flutter Web + package:web
음성 인식Web Speech API (SpeechRecognition)
백엔드Supabase Edge Function (Deno)
대화 메모리Supabase PostgreSQL conversation_messages
AIClaude Sonnet 4.6

핵심: Flutter Web에서 Web Speech API

Flutter Web에서 음성 인식을 사용하려면 package:web를 통해 브라우저 API에 직접 접근합니다:

import 'package:web/web.dart' as web;

class SpeechRecognitionService {
  web.SpeechRecognition? _recognition;

  void startListening(Function(String) onResult) {
    _recognition = web.SpeechRecognition();
    _recognition!.lang = 'ja-JP';
    _recognition!.continuous = false;
    _recognition!.interimResults = false;

    _recognition!.onresult = (web.SpeechRecognitionEvent event) {
      final transcript = event.results.item(0)!.item(0)!.transcript;
      onResult(transcript);
    }.toJS;

    _recognition!.start();
  }
}

핵심 포인트: 언어를 지정하려면 lang을 설정하고, 단일 발화만 캡처하려면 continuous: false를 사용합니다.

대화 메모리: conversation_messages 테이블

create table conversation_messages (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade,
  session_id text not null,
  role text not null check (role in ('user', 'assistant')),
  content text not null,
  created_at timestamptz default now()
);

session_id는 세션을 구분하면서 히스토리 연속성을 가능하게 합니다.

Edge Function: chat 액션

ai-assistant 기존 Edge Function에 chat 액션을 추가했습니다:

case 'chat': {
  const { message, sessionId, userId } = body;

  // Fetch last 10 messages for context
  const { data: history } = await supabase
    .from('conversation_messages')
    .select('role, content')
    .eq('session_id', sessionId)
    .order('created_at', { ascending: true })
    .limit(10);

  const messages = [
    ...(history || []),
    { role: 'user', content: message }
  ];

  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    messages,
  });

  const assistantMessage = response.content[0].text;

  await supabase.from('conversation_messages').insert([
    { user_id: userId, session_id: sessionId, role: 'user', content: message },
    { user_id: userId, session_id: sessionId, role: 'assistant', content: assistantMessage },
  ]);

  return new Response(JSON.stringify({ message: assistantMessage }));
}

history 배열을 Claude의 messages 매개변수에 직접 전달하면 장기 메모리를 무료로 얻을 수 있습니다.

주의사항

콜백을 위한 JS 인터옵

Flutter Web에서 SpeechRecognition 이벤트에 콜백을 할당할 때는 .toJS를 사용해야 합니다:

_recognition!.onresult = (event) { /* … */ }.toJS; // .toJS is required

RLS 정책 참조

conversation_messages에 Row Level Security를 설정할 때, 정책이 올바른 테이블을 참조하도록 해야 합니다. 프로젝트에서 auth.users를 직접 참조하는 대신 user_profiles 테이블을 사용하는 경우 42P01 오류가 발생합니다.

결론

  • package:web를 통해 Flutter Web에서 Web Speech API를 깔끔하게 사용할 수 있습니다.
  • Supabase + Edge Functions를 사용하면 약 50줄 정도로 지속적인 대화 메모리를 구현할 수 있습니다.
  • Claude API의 messages 배열은 대화 컨텍스트를 자연스럽게 처리합니다.

개인 프로젝트에 AI 채팅 기능을 추가하는 분들에게 도움이 되길 바랍니다.

공개적으로 빌드 중입니다.

0 조회
Back to Blog

관련 글

더 보기 »

LinkedIn 아니면 LinkeDone?

!LinkedIn 또는 LinkeDone용 커버 이미지?https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads...