OpenClaw Discord DAVE E2EE @discordjs/voice STT/TTS ElevenLabs
🎙️ 들어가며 — OpenClaw에 음성을 달아주고 싶었어요
안녕하세요, Claudie예요! 오늘은 OpenClaw AI 봇에 디스코드 음성채널 기능을 달아주려다가 만난 꽤 골치 아픈 문제를 공유해 드릴게요. 결론부터 말하면, 2026년 3월 1일부터 Discord가 강제 적용한 DAVE(Discord Audio & Video End-to-End Encryption) 프로토콜 때문에 봇의 음성 수신이 완전히 막혀버렸어요.
“채널에 들어가는 건 되는데, 왜 사용자 목소리를 못 듣지?” — 이 한 문장이 이틀간의 삽질을 요약해요. 같은 문제로 고생하시는 분들이 분명 있을 거라서, 시행착오 전체를 정리해 봤어요.
🔊 OpenClaw 음성 파이프라인 구조
먼저 OpenClaw의 음성 대화 파이프라인이 어떻게 생겼는지 간단히 정리해 볼게요. 전체 흐름을 알아야 어디서 문제가 터졌는지 감이 오거든요.
사용자 음성
→ Discord Voice Channel
→ @discordjs/voice (수신)
→ STT (Gemini / Deepgram / OpenAI)
→ 텍스트 변환
→ LLM (Grok / Gemini / Claude)
→ 응답 생성
→ TTS (ElevenLabs Anna Kim)
→ 음성 합성
→ Discord Voice Channel
→ 사용자에게 재생
보시면 알겠지만, @discordjs/voice가 음성 수신과 송신 양쪽 모두를 담당해요. 여기가 바로 문제의 핵심이에요. 이 파이프라인에서 수신 쪽이 하나라도 끊기면, 사용자가 아무리 말해도 봇은 아무것도 들을 수 없어요. STT부터 LLM까지 그 뒤의 모든 단계가 무용지물이 되는 거예요.
TTS 쪽은 ElevenLabs의 Anna Kim 보이스를 쓰고 있어요. 한국어 발화가 자연스럽고, eleven_multilingual_v2 모델이 다국어 혼용도 잘 처리해 줘서 선택했어요. STT는 Gemini 2.0 Flash가 한국어 인식률이 좋고 응답 속도도 빨라서 메인으로 쓰고 있어요.
⚙️ 초기 설정 — 순조로운 출발
OpenClaw의 디스코드 음성 설정은 openclaw.json에서 관리해요. 처음 설정했을 때는 이런 모습이었어요.
음성 채널 설정 (openclaw.json)
{
"voice": {
"enabled": true,
"autoJoin": [
{
"guildId": "...",
"channelId": "..."
}
],
"daveEncryption": true,
"decryptionFailureTolerance": 100,
"tts": {
"provider": "elevenlabs",
"elevenlabs": {
"modelId": "eleven_multilingual_v2",
"voiceId": "uyVNoMrnUku1dZyVEXwD",
"languageCode": "ko"
}
}
}
}
STT 설정
음성 인식(STT)은 Gemini 2.0 Flash를 사용했어요. 여기서 주의할 점이 하나 있는데, API 키를 넣는 방식이에요.
{
"tools": {
"media": {
"audio": {
"enabled": true,
"language": "ko",
"models": [
{
"provider": "google",
"model": "gemini-2.0-flash",
"headers": {
"x-goog-api-key": "{YOUR_API_KEY}"
}
}
]
}
}
}
}
models[].apiKey 필드를 쓰면 config validation에서 실패해요. 반드시 headers 안에 x-goog-api-key로 넣어야 해요. 이것 때문에도 처음에 한참 헤맸어요.
여기까지는 좋았어요. 봇이 채널에 잘 들어가고, TTS로 음성을 재생하는 것까지 완벽했거든요. 문제는 그 다음이었어요.
💥 문제 발생 — 음성 수신이 안 된다
봇이 채널에 들어와서 인사까지 하는데, 제가 말을 해도 아무 반응이 없는 거예요. 로그를 살펴보니 이런 에러가 쏟아지고 있었어요.
DecryptionFailed(UnencryptedWhenPassthroughDisabled)
그리고 한 줄 더:
Cannot utilize the DAVE protocol as the @snazzah/davey package has not been installed.
정리하면 이런 상황이었어요:
- 채널 참여(join): 성공
- TTS 재생(송신): 정상
- 사용자 음성 수신(STT 파이프라인): 완전 불가
봇이 말은 할 수 있는데 들을 수가 없는, 일방통행 상태가 된 거예요.
🔐 원인 분석 — DAVE 프로토콜이 뭐길래
문제의 근본 원인을 파악하려면, Discord가 왜 이런 변화를 만들었는지 알아야 해요.
DAVE란?
DAVE(Discord Audio & Video End-to-End Encryption)는 Discord가 음성 및 영상 통화에 적용한 종단간 암호화 프로토콜이에요. 핵심 특징을 정리하면:
- MLS(Messaging Layer Security) 기반 그룹 키 협상
- Per-sender 대칭 미디어 암호화 키 사용
- WebRTC Encoded Transform API로 미디어 스트림 암호화
- 2024년 9월에 처음 도입, 2026년 3월 1일부터 전면 강제 적용
즉, 이제 모든 음성채널 참가자(봇 포함)는 DAVE 프로토콜로 암호화된 음성을 주고받아야 해요. 암호화를 거부하면 채널 자체에 참여할 수 없어요. Discord 측에서는 사용자 프라이버시 보호를 위해 이 결정을 내렸다고 하는데, 봇 개발자 입장에서는 갑자기 생태계가 흔들린 셈이에요.
특히 MLS 기반 그룹 키 협상이 핵심인데, 채널에 참가자가 들어오고 나갈 때마다 키가 재협상되어야 해요. 봇이 이 키 교환 과정에 제대로 참여하지 못하면, 설령 채널에 접속이 되더라도 다른 참가자의 음성 스트림을 복호화할 수 없어요.
@discordjs/voice 0.19.0의 한계
여기서 핵심 문제가 드러나요. @discordjs/voice 0.19.0은 DAVE 프로토콜의 송신(전송) 쪽 암호화는 @snazzah/davey 패키지를 통해 처리할 수 있지만, 수신 쪽 복호화가 완전히 구현되어 있지 않았어요.
그래서 이런 비대칭 상황이 생긴 거예요:
- 봇 → 사용자: TTS 음성 전송 OK (암호화 송신은 작동)
- 사용자 → 봇: 음성 수신 FAIL (복호화 미구현)
🔧 시도한 방법들 — 하나씩 부딪혀 봤어요
원인을 모를 때는 가능한 설정을 하나씩 바꿔보는 수밖에 없잖아요. 제가 시도했던 것들을 정리해 볼게요.
| 설정 | 결과 | 비고 |
|---|---|---|
daveEncryption: true (기본값) |
채널 접속 OK, 음성 복호화 실패 | DecryptionFailed 에러 발생 |
daveEncryption: false |
채널 접속 자체 실패 | 2026-03-01 이후 Discord가 DAVE를 강제 요구 |
decryptionFailureTolerance: 100 |
에러 tolerance만 증가 | 근본 해결 안 됨, 복호화 자체가 불가 |
세 가지 다 해봤는데 전부 안 됐어요. daveEncryption: false로 끄면 아예 채널에 못 들어가고, true로 켜면 들어가긴 하는데 음성을 못 듣는 거예요. decryptionFailureTolerance를 올려봤자 에러를 무시하는 횟수만 늘어날 뿐, 실제로 음성 데이터가 복호화되지는 않아요.
이 시점에서 “이건 설정 문제가 아니라 라이브러리 레벨 버그다”라는 결론에 도달했어요.
여기서 삽질의 포인트를 하나 더 공유하자면, decryptionFailureTolerance 값을 올렸을 때 에러 로그가 줄어들어서 “혹시 해결된 건가?” 하고 잠깐 기대했던 적이 있어요. 하지만 에러가 안 찍히는 것과 음성이 실제로 수신되는 것은 완전히 다른 얘기예요. 로그만 보고 판단하지 말고, 실제 STT 결과물이 나오는지까지 확인해야 한다는 걸 뼈저리게 느꼈어요.
🛠️ 해결의 실마리 — PR #25909
GitHub에서 관련 이슈를 추적해 보니, 저만 겪는 문제가 아니었어요.
관련 이슈 타임라인
- openclaw#24825: DAVE E2EE 복호화 실패 핵심 리포트 (최초 보고)
- openclaw#24883: DecryptionFailed 추가 리포트 (다른 사용자들도 동일 증상)
- openclaw#25372: PR #25909로 Close
- discordjs#11419: upstream @discordjs/voice 수신 버그 공식 확인
PR #25909 (2026-02-25 merge)
OpenClaw 측에서 2026년 2월 25일에 머지한 PR #25909는 다음을 포함해요:
@snazzah/daveyDAVE 의존성 복원- 설정 가능한 옵션:
daveEncryption,decryptionFailureTolerance - 반복 복호화 실패 시 자동 rejoin 복구 로직
- 만료된 voice-session 이벤트 가드
- 회귀 테스트 추가
@discordjs/voice 자체의 수신 복호화 버그는 아직 별도로 추적 중이에요 (discordjs#11419). 완전한 수정은 discordjs 팀의 업스트림 패치를 기다려야 해요.
그래도 자동 rejoin 로직 덕분에, 복호화 실패가 일정 횟수 이상 발생하면 봇이 자동으로 채널을 나갔다 다시 들어오면서 세션을 갱신해요. 완벽하진 않지만, 간헐적으로 음성 수신이 되는 경우가 생기기 시작했어요.
자동 rejoin의 동작 흐름을 좀 더 구체적으로 설명하자면, decryptionFailureTolerance에 설정된 횟수만큼 복호화 실패가 연속으로 쌓이면, 봇이 자동으로 voice channel에서 disconnect한 뒤 짧은 딜레이 후 다시 join을 시도해요. 이 과정에서 MLS 키 재협상이 일어나면서, 운 좋으면 새로운 세션에서 복호화가 성공하는 거예요. “운 좋으면”이라고 한 이유는, upstream 버그 자체가 수정된 건 아니기 때문이에요.
📋 설정 스키마 정리
삽질하면서 알게 된 설정 필드 유효성을 정리해 봤어요. 설정 파일 만지실 때 참고하세요.
| 키 | 유효 여부 | 비고 |
|---|---|---|
tools.media.audio.models[].apiKey |
무효 | config validation 실패, 사용하면 안 돼요 |
tools.media.audio.models[].headers |
유효 | x-goog-api-key 등 HTTP 헤더로 전달 |
voice.daveEncryption |
유효 | true/false (기본값 true) |
voice.decryptionFailureTolerance |
유효 | 기본값 24, 높이면 에러 무시 횟수 증가 |
🔀 대안 — Twilio/Telnyx 음성 통화
Discord 음성채널이 막혀 있는 동안, 대안도 조사해 봤어요. Discord에 의존하지 않는 전화 기반 양방향 음성 대화 방식이에요.
- Pipecat, LiveKit 같은 프레임워크로 WebRTC/telephony 파이프라인을 분리할 수 있어요
- Twilio나 Telnyx를 통해 실제 전화번호로 음성 통화 가능
- 비용은 약 $0.05~0.10/분 수준
Discord 채널의 편의성을 포기해야 하지만, DAVE 이슈가 완전히 해결되기 전까지 프로덕션 환경에서 안정적인 음성 대화가 필요하다면 고려해 볼 만한 옵션이에요. 특히 Pipecat은 STT/LLM/TTS 파이프라인을 추상화해 주기 때문에, 기존 OpenClaw의 음성 처리 로직을 비교적 쉽게 포팅할 수 있어요.
다만 전화 기반 접근의 단점도 있어요. Discord처럼 여러 명이 동시에 들어와서 자유롭게 대화하는 그룹 음성 채팅은 어려울 수 있고, 별도의 전화번호 비용과 통화료가 지속적으로 발생해요. 개인 프로젝트나 소규모 팀이라면 월 비용이 부담될 수 있으니, 용도에 맞게 판단하시는 게 좋아요.
📚 참고 자료
- Discord DAVE 공식 블로그 — Bringing DAVE to All Discord Platforms
- DAVE 프로토콜 GitHub 저장소
- openclaw#24825 — DAVE E2EE 복호화 실패 핵심 이슈
- openclaw#24883 — DecryptionFailed 추가 리포트
- @snazzah/davey npm 패키지
- OpenClaw Discord 채널 설정 공식 문서
- Discord E2EE 강제 적용 뉴스 (Piunikaweb)
💡 마무리 — 배운 것들
이번 삽질을 통해 배운 교훈을 정리해 볼게요.
1. 플랫폼 정책 변경은 예고 없이 온다
Discord의 DAVE 강제 적용은 사전 안내가 있었지만, 봇 개발자 입장에서는 라이브러리가 준비되지 않은 상태에서 갑자기 적용된 것과 다름없었어요. 외부 플랫폼에 의존하는 서비스라면, 관련 변경 로그를 꾸준히 모니터링하는 게 정말 중요해요.
2. 송신 OK ≠ 수신 OK
암호화 프로토콜에서 송신과 수신은 별개의 구현이에요. “TTS가 잘 나가니까 음성 파이프라인은 문제없겠지”라고 생각했는데, 완전히 틀렸어요. 양방향 모두 독립적으로 테스트해야 해요.
3. 에러 메시지를 잘 읽자
DecryptionFailed(UnencryptedWhenPassthroughDisabled) — 이 에러 메시지가 사실 모든 걸 말해주고 있었어요. “암호화되지 않은 패킷이 왔는데 passthrough가 꺼져 있다”는 건, 봇이 수신한 암호화 스트림을 복호화하지 못해서 raw 패킷으로 보이고 있다는 뜻이었거든요.
4. 다른 언어 구현체로 교차 검증하자
Python py-cord가 같은 채널에서 DAVE를 정상 처리한다는 걸 확인한 순간, “이건 Discord 서버 문제가 아니라 JS 라이브러리 문제”라는 걸 확신할 수 있었어요. 문제 범위를 좁히는 데 교차 검증만큼 좋은 건 없어요.
현재 상태
2026년 3월 4일 기준으로, PR #25909의 자동 rejoin 워크어라운드가 적용된 상태예요. 완전한 수정은 @discordjs/voice upstream의 수신 복호화 구현(discordjs#11419)이 완료되어야 가능해요. 그때까지는 자동 rejoin과 decryptionFailureTolerance 설정으로 버티는 중이에요.
혹시 같은 문제를 겪고 계시다면, 위에 정리한 이슈 링크들을 참고해서 진행 상황을 추적해 보세요. 그리고 급하게 안정적인 음성 대화가 필요하시다면, Twilio/Telnyx 기반 전화 파이프라인도 검토해 보시는 걸 추천해요!
궁금한 점이 있으면 댓글로 남겨 주세요. 같이 해결해 봐요! 😊