음성봇을 스텁부터 만든 이유 — 테스트로 설계를 먼저 굳히다

시리즈 · 음성봇 직접 만들기 — 실시간 음성봇의 뼈대를 바닥부터 만든 8편 기록입니다. 1. 끼어들기(barge-in) — 재생 중인 TTS를 실시간으로 끊는 법 2. "말이 끝났다"는 판단 — VAD 엔드포인터 3. 엔진을 코드 수정 없이 바꾸는 인터페이스 설계 4. 로컬 LLM 대화 두뇌와 도구 호출 루프 5. 매 턴 지연시간을 재는 이유 6. 브라우저 마이크에서 서버까지 — 오디오 프로토콜 7. 로컬 Whisper STT 연결하기 8. 스텁부터 만든 이유 — 테스트 우선 설계 (완결) — 지금 보는 글 돌아보며 시작하는 마지막 편 이 시리즈를 시작할 때부터 정해 둔 규칙이 하나 있었습니다. 진짜 음성 인식 모델과 진짜 합성 모델이 준비되기 전까지는, 그 둘을 흉내만 내는 가짜 엔진으로 전체 구조를 먼저 돌려본다는 규칙입니다. 여덟 편을 거치며 끼어들기 처리, 상태 흐름, 프로토콜, 엔진 인터페이스를 하나씩 다뤘는데, 그 모든 것이 사실 이 가짜 엔진 위에서 검증됐습니다. 지금 코드를 다시 열어 보면 그때는 별생각 없이 세운 규칙이 결과적으로 시리즈 전체의 진행 속도를 결정했다는 게 눈에 들어옵니다. 이번 편에서는 왜 굳이 가짜부터 만들었는지, 그리고 그 선택이 시리즈 전체에 어떤 영향을 남겼는지 정리하면서 이 시리즈를 마무리하려 합니다. 실제 모델을 기다리지 않기로 한 이유 음성봇의 핵심 로직은 사실 음성 인식이나 음성 합성 자체가 아닙니다. 사용자가 말을 시작했는지 끝냈는지 판단하는 상태머신, 합성 중에 끼어들기가 들어왔을 때 스트림을 멈추는 동시성 처리, 이벤트가 순서대로 오가는 흐름 제어 같은 것들이 훨씬 까다롭습니다. 반면 실제 인식·합성 모델은 무겁고 설치가 까다롭고 실행 속도도 느립니다. 이 두 가지를 한꺼번에 붙잡고 있으면 어려운 동시성 버그를 실제 모델의 로딩 시간과 뒤섞어 디버깅하게 됩니다. 모델이 느려서 반응이 늦은 건지, 상태머신이 잘못 짜여서 늦은 건지 구분이 안 되는 상황을 미...

디스코드 구글캘린더 연동 봇 만들기: 카톡 캡처 OCR 자동 등록 시스템 구축


Discord 구글 캘린더 봇을 직접 만들어 운영하면 일정 입력이라는 잡일이 거의 사라집니다. 입력 방법은 두 가지입니다. 첫째, 텍스트 한 줄 — 채널에 "내일 오후 3시 강남역 미팅"처럼 적기만 하면 됩니다. 둘째, 이미지 캡처 — 카카오톡으로 받은 모임 안내, 학원 시간표, 행사 포스터를 그대로 드래그하면 됩니다. 둘 다 Claude CLI가 읽어 Google Calendar에 자동 등록합니다. 매번 손으로 캘린더 앱을 열어 제목·시간·장소를 입력하던 마찰이 사라집니다. 본인은 이 봇으로 누적 [N]건+ 일정을 자동 등록하며 운영 중입니다.

아래에서는 Discord 메시지(텍스트 또는 이미지) → Claude CLI → Google Calendar MCP 파이프라인을 하나씩 짜 나갑니다. macOS + Python + discord.py + Claude Code CLI + Google Calendar MCP + SQLite 조합이고, 한 시간 남짓이면 본인 환경에서 그대로 따라 만들 수 있습니다. 직전 편 Discord 유튜브 봇 → Notion 아카이브와 같은 "Discord = 인박스" 패턴의 캘린더 버전입니다.

텍스트 한 줄이든 카톡 캡처든 Google Calendar에 자동 등록 + 수정·삭제 버튼 결과 임베드

텍스트 한 줄이든 카톡 캡처든 → Google Calendar 자동 등록 + [수정]/[삭제] 버튼 결과 임베드 (사용자 본인 실제 화면)

1. 왜 이 조합인가

구조 자체는 복잡하지 않습니다. 무엇이든 일단 Discord 채널에 던져 두면, 그걸 Claude가 읽어 일정으로 정리한 뒤 Google Calendar에 옮겨 박아 줍니다. 채팅창은 그때그때 떠오른 일정을 아무렇게나 흘려 넣는 자리이고, 캘린더는 그렇게 들어온 것들이 제대로 자리 잡는 곳입니다. 사람이 하는 일은 텍스트 한 줄을 적거나 캡처 한 장을 드래그하는 것까지입니다.

입력은 두 경로로 나뉩니다. 텍스트 메시지는 이미지 처리 없이 바로 Claude CLI가 일정으로 구조화합니다. "담주 수요일 7시 저녁 약속"처럼 적어도 됩니다. 이미지 첨부는 Claude CLI의 비전이 카카오톡 캡처나 안내문을 직접 해석합니다. 이미지 일정을 읽을 때 보통 별도 OCR 엔진을 붙이지만, 이 봇은 그러지 않습니다. Claude 비전이 제목·시작·종료·장소를 바로 뽑아내므로, OCR 텍스트를 다시 파싱하는 중간 단계가 없어 한국어 안내문처럼 레이아웃이 제각각인 이미지에도 강합니다. 두 경로 모두 같은 일정 JSON으로 합류해 동일하게 등록됩니다.

등록도 Google Calendar API를 직접 다루지 않습니다. Claude Code CLI에 연결된 Google Calendar MCP가 이벤트 생성을 대신하므로, OAuth 토큰을 코드로 관리할 필요가 없습니다. 봇은 "이런 일정을 등록해줘"라고 Claude에게 위임하고 결과만 받습니다.

마지막으로, 잘못 읽었을 때를 대비합니다. 등록 결과는 Discord 임베드로 돌아오고 [수정]/[삭제] 버튼이 붙습니다. 시간을 잘못 잡았으면 그 임베드에 "수정: 내일 14시"라고 답장 한 줄만 보내면 캘린더 이벤트가 갱신됩니다.

카톡 일정 캡처를 Discord 채널에 드래그하면 몇 초 뒤 Google Calendar에 자동 등록되는 흐름

카톡 일정 캡처를 채널에 드래그 → 몇 초 뒤 Google Calendar에 자동 등록되는 흐름 (사용자 본인 실제 화면 · GIF)

결과 임베드의 수정·삭제 버튼 — 답장 한 줄로 캘린더 이벤트를 갱신하는 화면

결과 임베드의 [수정]/[삭제] 버튼 — 답장 "수정: 내일 14시" 한 줄로도 캘린더 이벤트가 갱신됩니다 (사용자 본인 실제 화면)

2. 사전 준비물 체크리스트

  • macOS, Python 3.11+, Discord 계정과 전용 채널 1개
  • Discord 봇(Developer Portal) — Message Content Intent 활성화
  • Claude Code CLI 설치 + claude.ai Google Calendar MCP 연결 (claude mcp list로 연결 상태 확인)
  • Google 계정(Calendar) — Claude MCP에 연결
  • SQLite (macOS 기본 포함)
  • launchd (상시 실행용, macOS 기본)

준비물에 별도 OCR 서비스나 Google Cloud API 키 발급이 없다는 점이 이 구성의 장점입니다. 비전과 캘린더 등록을 모두 Claude CLI 한 곳으로 모았기 때문입니다.

3. 단계별 구현

3.1 Discord 봇 생성 + 전용 채널

Discord Developer Portal에서 봇을 만들고 Message Content Intent를 활성화하는 화면

Discord Developer Portal에서 봇 생성 + Message Content Intent 활성화 (사용자 본인 실제 화면)

Discord Developer Portal에서 새 애플리케이션을 만들고 Bot 탭에서 토큰을 발급합니다. 반드시 Privileged Gateway Intents에서 Message Content Intent를 켜야 메시지 본문과 첨부 이미지를 읽을 수 있습니다. OAuth2 URL Generator로 봇을 서버에 초대한 뒤, 일정 전용 채널을 하나 만들고 개발자 모드에서 채널 ID를 복사합니다. 토큰과 채널 ID는 .env의 DISCORD_TOKEN, DISCORD_CHANNEL_ID에만 저장합니다.

3.2 Claude CLI + Google Calendar MCP 연결 확인

Claude CLI에 Google Calendar MCP가 연결된 것을 확인하는 화면

Claude CLI에 Google Calendar MCP가 연결된 확인 화면 (사용자 본인 실제 화면)

이 봇의 등록 단계는 전적으로 Calendar MCP에 의존합니다. 터미널에서 다음으로 연결 상태를 먼저 확인합니다.


claude mcp list   # claude.ai Google Calendar 가 Connected 인지 확인

여기서 Calendar MCP가 연결돼 있지 않으면 추출은 되더라도 등록이 전부 실패하므로, 구현 전에 반드시 연결 상태를 확보합니다.

3.3 추출 호출 — 메시지에서 일정 JSON 뽑기

봇은 on_message로 전용 채널의 메시지를 감시합니다. 텍스트만 있는 메시지는 다운로드 없이 본문을 그대로 프롬프트에 넣습니다. 이미지 첨부가 있으면 첨부 폴더에 내려받고, 그 경로를 프롬프트에 넣어 Claude CLI 비전이 읽도록 합니다. 두 경우 모두 같은 추출 프롬프트를 거쳐 동일한 일정 JSON으로 수렴합니다.


python3 -m venv .venv && source .venv/bin/activate
pip install discord.py aiohttp python-dotenv

추출 프롬프트에는 일정 JSON 스키마와 함께 현재 KST 시각을 반드시 주입합니다. 그래야 "내일 2시", "담주 화요일"처럼 상대 표현만 있는 일정도 절대 날짜로 변환됩니다. 반환된 JSON은 제목·시작·종료·종일 여부·장소·신뢰도(confidence)·되묻기 필요 여부(needs_clarification) 필드로 파싱합니다. 되묻기가 필요한 경우(상대 날짜가 모호하거나 시간이 비어 있을 때)에는 바로 등록하지 않고 채널에 한 번 되묻습니다.

3.4 등록 호출 — Calendar MCP + 결과 임베드/버튼

추출한 일정 JSON을 Claude CLI + Calendar MCP에 넘겨 이벤트를 생성합니다. 생성된 이벤트의 링크를 받아 Discord 결과 임베드를 만들고, discord.py의 View로 [수정]/[삭제] 버튼을 붙입니다. 신뢰도가 CONFIDENCE_THRESHOLD보다 낮으면 임베드에 주의 표시를 남겨 사람이 한 번 더 검토하도록 유도합니다(등록 자체는 진행). 잘못 등록됐다면 임베드에 답장으로 "수정: ..." 한 줄을 보내거나 버튼을 눌러 즉시 고칠 수 있습니다.

3.5 SQLite 상태 — 캐치업 커서 + 매핑

state.py는 두 가지를 SQLite에 저장합니다. 첫째, 마지막으로 처리한 메시지 ID를 커서로 두어 봇이 재시작하면 그 이후 메시지만 캐치업합니다. 둘째, Discord 임베드 메시지 ID와 캘린더 이벤트 ID의 매핑을 저장해, 나중에 수정·삭제 요청이 들어왔을 때 어떤 이벤트를 손볼지 특정하고 같은 일정이 두 번 등록되는 것을 막습니다.

3.6 상시 실행 — launchd 등록

봇을 launchd로 등록하면 로그인 후 항상 떠 있게 됩니다. 다만 macOS에서 Google Drive(CloudStorage) 경로를 작업 디렉터리로 두면 권한 문제로 봇이 파일을 못 읽는 함정이 있는데, 이는 4장에서 다룹니다. 등록 후에는 전용 채널에 카톡 캡처를 한 장 던져 결과 임베드와 실제 캘린더 등록을 확인합니다.

launchd에 등록해 봇이 상시 백그라운드로 실행되는 상태

launchd 등록으로 봇이 상시 백그라운드 실행되는 상태 (사용자 본인 실제 화면)

4. 자주 만나는 에러 3가지

4.1 launchd가 Google Drive 폴더에 접근하지 못해 봇이 멈춤

launchd로 상시 실행할 때 작업 디렉터리를 Google Drive(CloudStorage) 경로로 두면, macOS 개인정보 보호(TCC) 권한에 막혀 봇이 코드·데이터 파일을 읽지 못합니다. 해결책은 운영 사본을 로컬(~/.discord-calendar/)에 두고 거기서 봇을 구동하는 것입니다. 원본은 Google Drive에서 편집하고 sync.sh로 로컬에 동기화합니다. launchd plist의 WorkingDirectory도 로컬 경로로 지정합니다.

4.2 "내일", "담주 화" 같은 상대 날짜를 엉뚱한 날로 등록

캡처나 텍스트에 절대 날짜가 없고 상대 표현만 있으면 캘린더에 잘못된 날짜로 등록됩니다. 추출 프롬프트에 현재 KST 시각을 주입해 상대 표현을 절대 날짜로 변환하고, 그래도 모호하면 needs_clarification으로 표시해 등록을 보류하고 채널에 되묻도록 했습니다.

4.3 절전/깨어남 이후 봇이 좀비가 되어 그 사이 메시지 누락

macOS가 절전에서 깨어나면 Discord 커넥션이 끊겨, 그 사이 올라온 캡처가 처리되지 않을 수 있습니다. SQLite 캐치업 커서로 마지막 처리 메시지 이후를 재처리하고, sleepwatcher의 ~/.wakeup 훅으로 깨어날 때 봇을 자동 재시작해 누락을 막습니다.

마치며

이 봇을 쓰고 나서 달라진 건, 카톡으로 받은 일정 캡처를 더 이상 손으로 캘린더에 옮겨 적지 않게 됐다는 점입니다. 채널에 캡처 한 장을 던지면 몇 초 뒤 캘린더에 일정이 박혀 있고, 잘못 읽었더라도 답장 한 줄로 고칩니다.

운영자 운영 노트 (실측 1차 정보 — 발행 전 교체): 누적 등록 [N]건. 가장 까다로웠던 부분은 [상대 날짜 되묻기 / 저화질 캡처 인식 등 실제 케이스]. 비전 OCR이 한국어 안내문을 [어디까지] 읽었는지 소회.

이 글의 봇은 Discord 콘텐츠 아카이브 시리즈의 한 편입니다. 뉴스 URL·인스타 저장 같은 변형 봇은 후속 편에서 다룰 예정입니다. 본문 중 명령어가 동작하지 않으면 Discord.py 공식 문서와 Claude Code CLI 문서를 함께 확인하기를 권합니다.

댓글

  1. 작성해주신 글을 참고해서 저도 디스코드와 구글 캘린더를 연동하는 에이전트를 만들어봤습니다! 캡처 사진 인식률도 생각보다 훨씬 좋고, 텍스트 추출도 정확해서 정말 만족스럽게 사용 중입니다. 좋은 정보 공유해주셔서 감사합니다!

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)

유튜브 노션 자동 저장 봇 만들기 — Claude CLI MCP + Discord 233일 운영기