AI 에이전트와 디스코드 음성 소통하기 (2)

AI 에이전트와 음성 소통 가능할까?

결론부터 말하면 가능합니다만, 마블 영화 속 자비스가 아닌 '가능'한 수준입니다.

실제 사람과 대화하는 수준은 아직 구현하는 것이 어렵다고 판단됩니다. (일반적인 환경 한정)
현재 디스코드 로그인 및 음성 채널 입장을 통해 소통까지 가능한 것을 확인했습니다.
해당 내용을 진행하면서 생긴 에러 및 진단 과정을 그대로 정리해봤습니다.

** 해당 내용은 AI 에이전트에게 그동안 진행했던 내용을 글로 정리해달라는 요청으로 나온 결과물입니다. 일부 내용만 수정했으며 중요한 과정들만 정리했습니다.


방향 설정: '음성 메시지'가 아니라 '음성 채널'

처음에는 디스코드 음성 메시지(녹음 노트)를 봇이 받아 처리하는 방식을 생각했습니다. 그런데 디스코드 음성 메시지는 모바일 앱에서만 녹음·전송이 됩니다. 윈도우·맥 데스크톱에서는 재생만 되고 녹음 버튼 자체가 없습니다. 데스크톱에서 마이크 아이콘이 안 보이는 것은 정상입니다.

게다가 음성 메시지는 텍스트 채널에 붙는 첨부라 실시간 처리에는 적합하지 않습니다. 실시간 음성 대화가 목적이라면 음성 채널(Voice Channel) 방식이 맞습니다.

전체 흐름은 다음과 같습니다.

사용자 발화(음성 채널)
  → 봇이 수신(opus) → PCM 변환
  → STT(음성 → 텍스트)
  → Claude 에이전트 → 응답 텍스트
  → TTS(텍스트 → 음성)
  → 봇이 음성 채널에서 재생

에이전트는 그대로 두고 앞뒤에 음성 입출력만 연결하는 구조입니다.


사용 스택

  • 봇 프레임워크: Node.js discord.js + @discordjs/voice

    • 파이썬(discord.py)은 봇의 음성 수신을 정식 지원하지 않습니다. 음성 채널 방식의 핵심이 수신이라 Node를 선택했습니다.

  • STT: Deepgram (한국어 지원, 지연 적음)

  • TTS: ElevenLabs (eleven_multilingual_v2 모델, 한국어 지원)

  • Claude 연결: 기존 Claude CLI 사용

CLI를 봇이 매번 직접 호출하면 느리고 불안정합니다. 그래서 CLI를 작은 HTTP 서버로 감싸두고, 봇은 텍스트만 전달하는 구조로 분리했습니다. 봇과 에이전트가 분리되어 문제 추적도 수월합니다.

CLI 호출부는 한 곳뿐이며, 환경마다 명령어가 다를 수 있어 이 부분만 수정하면 됩니다.

javascript

function buildAgentCommand(userText) {
  // 비대화형(headless) 방식이 안정적입니다
  return { cmd: "claude", args: ["-p", userText, "--output-format", "text"] };
}

터미널에서 claude -p "안녕"을 실행해 한국어 응답이 오면 CLI 연동은 완료된 것입니다.


설치 및 키 발급

이 구간은 무난하게 진행됩니다.

bash

cd ~/Downloads/discord-voice-agent
npm install          # deprecated 경고는 무시. "added N packages"가 뜨면 성공
node -v              # Node 버전 확인

npm install 후 나오는 deprecated 경고나 high severity vulnerabilities는 대부분 내부 의존성 문제이므로 무시해도 됩니다. 단, npm audit fix --force는 멀쩡한 코드를 깨뜨릴 수 있으니 실행하지 않습니다.

키는 세 곳에서 발급합니다.

  • 디스코드 봇 토큰: Developer Portal에서 봇 생성 후, OAuth2로 서버 초대(권한: Connect, Speak, Use Voice Activity)

  • Deepgram API 키: 가입 시 무료 크레딧 제공

  • ElevenLabs API 키 + 보이스 ID: TTS용. 키 권한은 '텍스트 음성 변환'만 켜면 충분

API 키는 비밀번호와 동일합니다. 발급 후 메모장에만 복사하고, 채팅창이나 코드에 그대로 붙여넣지 않습니다. 노출되었다면 폐기 후 재발급하는 것이 안전합니다.

여기에 디스코드 서버 ID음성 채널 ID까지 챙겨 .env에 입력합니다(개발자 모드를 켜야 우클릭 → 'ID 복사'가 나옵니다).

bash

cp .env.example .env
open -e .env     # 값 입력 후 저장. = 양옆 띄어쓰기 금지

실행: 절반의 성공, 그리고 AbortError

봇은 터미널 두 개로 실행합니다. 하나는 에이전트 서버, 하나는 봇입니다.

bash

# 터미널 1
npm run agent     # "Claude 에이전트 래퍼 서버 실행 중..." 출력 후 그대로 둠

# 터미널 2
npm start

결과는 절반의 성공이었습니다.

로그인됨: Muninn#7430
음성 연결 실패: AbortError: The operation was aborted

봇이 디스코드에 로그인까지는 성공했습니다. 토큰·키·코드가 정상이라는 의미입니다. 그러나 음성 채널 연결에서 20초 타임아웃이 발생했습니다. 디스코드 화면에는 봇이 채널에 들어와 있는데도 음성에 반응하지 않았습니다.

문제 진단은 여기서부터 시작됐습니다.


진단: 추측 대신 로그 삽입

처음에는 흔한 원인들을 순서대로 점검했습니다.

  • 서버/채널 ID 오기입? → 확인 결과 정상

  • 암호화 라이브러리 누락?npm install sodium-native 설치, 동일하게 실패

추측으로는 해결되지 않아, 음성 연결 상태 변화를 단계별로 출력하는 로그를 봇에 삽입했습니다. 디스코드 음성 연결은 signalling → connecting → ready 순으로 진행되므로, 멈추는 지점을 보면 원인을 특정할 수 있습니다.

javascript

connection.on("stateChange", (oldS, newS) => {
  console.log(`[진단] 음성상태: ${oldS.status} -> ${newS.status}`);
});

다시 실행하자 패턴이 드러났습니다.

[진단] 서버: RavenHold / 채널: 일반 (type=2)   ← ID는 정상
[진단] 음성상태: signalling -> connecting
[진단] 음성상태: connecting -> signalling
[진단] 음성상태: signalling -> connecting
[진단] 음성상태: connecting -> signalling
...
음성 연결 실패: The operation was aborted
[진단] 마지막 상태: signalling

connectingsignalling 사이를 무한 반복합니다. 이는 시그널링(연결 협상)은 되지만 실제 음성 데이터가 흐르는 UDP 통로가 뚫리지 않는 전형적인 증상입니다. 디스코드 음성은 협상을 WebSocket으로, 오디오를 UDP로 전송합니다. UDP가 막히니 connecting을 넘기지 못하고 재시도를 반복하다 타임아웃에 걸린 것입니다.

동일 증상이 @discordjs/voice 특정 버전에서도 보고되어 있습니다. 라이브러리 버전을 바꿔도 해결되지 않는 것까지 확인했습니다. 코드 문제가 아니라 네트워크 레이어에서 UDP가 가로채이는 문제라는 의미입니다.


원인 추적: utun 10개

UDP가 막힌다면 네트워크를 점검해야 합니다. 가상 네트워크 장치를 확인했습니다.

bash

ifconfig | grep utun

utun0부터 utun9까지, 가상 터널 장치가 10개 떠 있었습니다. 보통 VPN 하나당 1~2개인데 10개는 비정상입니다.

다만 VPN 목록은 비어 있었습니다(scutil --nc list). 일반 VPN 앱은 아니라는 뜻입니다. 프로세스를 확인하자 원인이 드러났습니다.

bash

ps aux | grep -iE "proxy|container"
# /usr/libexec/containermanagerd ... --bundle-container-mode=proxy
# /usr/libexec/networkserviceproxy

원인은 OpenClaw 에이전트 인프라의 컨테이너 프록시 모드(--bundle-container-mode=proxy)였습니다. 이 프로세스가 시스템 전체 UDP를 가로채고 있었습니다. 직접 구성한 에이전트 환경의 결과였습니다.

확정을 위해 휴대폰 핫스팟으로 네트워크를 통째로 바꿔 테스트했으나 동일하게 실패했습니다. 인터넷 경로를 바꿔도 안 된다는 것은, 원인이 공유기가 아니라 맥북 내부에서 UDP를 점유하는 프록시 계층이라는 결정적 증거였습니다.


결론: 봇은 완성됐고, 환경이 맞지 않았을 뿐

여기서 작업을 멈췄습니다. 이유는 명확합니다.

원인인 containermanagerd는 root 권한으로 실행되는 시스템 프로세스이며, 직접 구축한 OpenClaw 에이전트 인프라의 핵심입니다. 디스코드 음성 봇을 확인하려고 이를 건드렸다가 인프라가 꼬이면 복구가 더 큰 일이 됩니다. 위험 대비 실익이 적은 선택입니다.

중요한 점은 봇은 사실상 완성되었다는 것입니다.

  • 코드 ✅ (로그인까지 정상)

  • 키·토큰·설정 ✅

  • 막힌 것은 이 맥북 특유의 네트워크 환경 하나

OpenClaw 프록시가 없는 환경(다른 컴퓨터 또는 저가 VPS)에 이 폴더만 옮기면 바로 동작합니다. 지금까지의 작업은 그대로 활용됩니다.

** 이후 같은 과정을 진행해 아이맥으로 해당 내용을 다시 셋팅해서 음성대화를 하는 것을 성공했습니다. 물론 에이전트의 TTS 음성이나 사이트에서 읽어오는 과정에서 이슈들이 있었지만, 앞의 과정이 더 중요한 과정이라고 생각되어 여기까지만 정리했습니다. (응답이 느리기도 합니다)

AI 에이전트와 음성대화를 생각하셨던 분들에게 도움이 되셨기를 바랍니다.

정리

  1. 로그인 성공 ≠ 음성 연결 성공. 디스코드 봇은 게이트웨이(WebSocket)와 음성(UDP)이 별개 통로입니다.

  2. AbortError + connecting↔signalling 무한 반복 = UDP 차단 신호. 코드보다 네트워크를 먼저 점검합니다.

  3. 막힐 때는 stateChange 로그를 삽입합니다. '어디서 멈추는가'가 원인의 절반을 알려줍니다.

  4. 에이전트 프록시와 디스코드 음성은 같은 UDP를 두고 충돌할 수 있습니다. 한 기기에서 둘 다 운영하려면 격리가 필요합니다.

  5. 24시간 운영할 봇이라면 처음부터 별도 서버가 정석입니다. 작업용 머신의 복잡한 네트워크와 엮이지 않습니다.

긴 글 읽어주셔서 감사합니다.

1
1개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.