1편에서 디스코드 봇 삐삐를 결국 안 쓰게 됐다는 얘기까지 했다. 나도 디스코드를 자주 안 열고 와이프는 아예 안 쓴다. 둘 다 하루 종일 켜 두는 앱은 카톡뿐이라 삐삐를 카톡으로 옮겼음.
원하는 그림
나, 와이프, 삐삐 셋만 있는 단톡방 “삐삐네”. 사람끼리 떠드는 방이 아니라 삐삐한테 시키는 방이다.
(07:30) 삐삐: 좋은 아침이에요. 10/29(목) …
나: 내일 3시에 치과 있어 기록해줘
삐삐: 네, 10/30(금) 15:00 치과 기록했어요.
와이프: 토요일 11시 미용실 기록 좀 해줘
삐삐: 네, 11/1(토) 11:00 미용실 기록했어요.
나: 순두부찌개 레시피가 뭐였지?
삐삐: (요약 + 출처 링크)
일정·메모·웹검색 같은 도구는 디스코드 삐삐와 똑같이 쓴다. 다른 건 와이프도 직접 시킬 수 있다는 거.
카톡에는 단톡방 봇 API가 없다
먼저 공식 방법부터 찾아봤는데 없었다(2026년 10월 기준). 카카오 개발자 메시지 API로는 “나에게 보내기”나 친구 1:1까지만 된다. 카톡 안에 들어온 AI 챗봇은 카카오 것이라 내 봇을 넣을 수 없다.
그래서 많이들 쓰는 방법으로 갔다. 안드로이드에서 카톡 알림을 읽어서 답장하는 앱, 메신저봇R이다.
- 봇용 카톡 계정을 하나 만들어서 안드로이드 기기에 로그인한다.
- 그 계정을 단톡방에 초대한다.
- 방에 메시지가 오면 알림이 뜬다. 메신저봇R의 JS 스크립트가 그 알림을 읽고 알림의 “답장” 기능으로 답을 보낸다.
카톡 앱을 건드리는 게 아니라 알림만 쓰는 방식이라 루팅도 필요 없다. 대신 비공식이라 언제든 막힐 수 있다는 건 알고 시작함.
봇 계정 번호
카톡 계정을 만들려면 전화번호가 있어야 한다. 해외 문자 수신용 가상번호로 때워 볼까 했는데, 카톡은 기기를 바꾸거나 재설치할 때 번호로 다시 인증한다. 번호가 사라지면 계정도 같이 날아간다. 그래서 알뜰폰 eSIM을 하나 개통했다. 제일 싼 요금제로 유지만 하면 된다.
나는 SKT 7Mobile의 1,900원짜리 esim 요금제를 선택했음.
서버도 공짜로 쓰는 판에 번호만 필요한 부분에다 비싼걸 유지할 필요가 없음.
물론 업무용 서브 번호가 필요하긴 했는데 이왕 잘됐다.

봇 계정은 ID 검색 허용도 꺼 뒀다. 메신저봇R은 방이나 사람의 고유 ID를 주지 않고 방 이름과 보낸 사람 표시 이름만 준다. 그러니 모르는 사람이 같은 이름으로 방을 만들어 봇을 초대하는 경우는 애초에 막아 두는 게 낫다.
안드로이드는 어디서 돌리나
후보는 셋이었다.
- 안 쓰는 폰 - 있긴 한데 너무 오래됐다.
- 서버(GCP e2-micro) - 메모리가 1GB다. 게다가 E2 머신은 중첩 가상화가 안 돼서 에뮬레이터를 못 올린다.
- 맥북의 안드로이드 에뮬레이터 - Android Studio로 Android 14 에뮬레이터를 하나 만들어서 카톡 + 메신저봇R을 깔았다.
맥북으로 정했음(언리얼 개발자라 사실 잘 쓰지도 않음). 본 작업 전에 입력을 그대로 따라 하는 메아리 봇부터 돌려 봤다.

방에 “안녕”을 쳤더니 바로 “메아리: 안녕”이 왔다. 이 시험으로 확인한 건 이렇다.
- 알림 답장은 잘 됨
- 알림에 찍히는 보낸 사람 이름은 카톡 프로필 이름 그대로다. 이걸로 누가 말했는지 구분함
- 스크립트를 재컴파일해도 방에 먼저 말을 거는
bot.send가 된다. - 에뮬레이터를 재부팅하면, 방에 메시지가 한 번 와야 먼저 보내기가 된다. 알림에 답장 정보가 붙어 와야 보낼 수 있는 구조라 그렇다. 그래서 스크립트가 지금 보낼 수 있는지(
ready)를 서버에 알려 주고 10분 넘게 못 보내는 상태면 디스코드로 “방에 아무 말이나 한 번 보내 주세요”라고 알리게 했음
구조
[맥북 · Android 에뮬레이터] [GCP e2-micro · 삐삐 프로세스]
카카오톡 (봇 계정)
메신저봇R 스크립트 ─ POST /kakao/message ──▶ 카톡 창구 (aiohttp)
◀───────────── {reply} ──────────── └▶ Brain (디스코드와 같은 세션) ─▶ 도구
─ GET /kakao/outbox (15초마다) ──▶ 보낼 목록 ◀─ 07:30 브리핑 · 늦은 답
─ POST /kakao/outbox/ack ─────────▶
Tailscale ◀════════════ 비공개 망 ════════════▶ Tailscale

서버에 공개 포트를 열지 않는다
에뮬레이터가 서버에 붙으려면 길이 있어야 하는데, 가족 일정이 오가는 창구를 인터넷에 열어 두긴 싫었다. 서버와 에뮬레이터에 Tailscale을 깔아서 둘만 있는 사설망으로 묶었다. 서버의 카톡 창구는 Tailscale 주소에만 바인딩한다. 설정에 다른 주소를 넣으면 아예 안 뜨게 막아 뒀다.
TAILNET = (ipaddress.ip_network("100.64.0.0/10"),
ipaddress.ip_network("fd7a:115c:a1e0::/48"))
if not any(ip in net for net in TAILNET):
raise KakaoConfigError(f"KAKAO_LISTEN이 Tailscale 대역이 아님: {host} — 그 밖의 주소에는 열지 않는다")
Tailscale 망 안은 WireGuard로 암호화되니 http로 충분하다. 다만 같은 망에 다른 기기가 붙을 수도 있으니 토큰 헤더를 하나 더 둔다.
@web.middleware
async def auth(request, handler):
got = request.headers.get("X-Pippi-Token", "").encode("utf-8", "surrogateescape")
if not hmac.compare_digest(got, token):
raise web.HTTPUnauthorized()
return await handler(request)
서버 쪽 설치는 Tailscale 공식 apt 저장소로 깔고 tailscale up --accept-dns=false 한 줄이면 끝이다. DNS 옵션은 서버 DNS 설정을 건드리지 않으려고 껐다. 하나 놓치기 쉬운 게 있는데, 관리 화면에서 키 만료(Disable key expiry)를 꺼야 한다. 안 그러면 180일 뒤에 서버가 망에서 조용히 빠짐
메시지 받고 답하기
스크립트 쪽은 단순하다. 우리 방 메시지면 서버에 넘기고 받은 답으로 답장. 답을 기다리는 동안 다른 알림이 막히지 않게 스레드로 돌림
bot.addListener(Event.MESSAGE, function (msg) {
if (!configured || String(msg.room) !== ROOM) return;
var body = {room: String(msg.room), sender: String(msg.author.name), text: String(msg.content)};
new java.lang.Thread(new java.lang.Runnable({
run: function () {
try {
var res = call("POST", "/kakao/message", body, MESSAGE_TIMEOUT_MS);
if (res.reply) msg.reply(res.reply);
} catch (e) {
Log.i("메시지 처리 실패: " + e);
}
}
})).start();
});

웹검색이 붙으면 답이 오래 걸릴 때가 있다. 서버는 90초 안에 답이 안 나오면 일단 “조금 걸려요, 찾으면 바로 보낼게요.”로 답한다. 실제 답은 다 만들어지면 보낼 목록(outbox)에 넣는다. 스크립트가 15초마다 이 목록을 가져가서 방에 보내고 보냈다고 ack를 돌려준다. 보낸 id는 1시간 기억해 둔다. ack가 중간에 실패해서 같은 항목이 다시 와도 두 번 보내지 않게 하려는 것
07:30 아침 브리핑도 같은 목록으로 나간다. 카톡 브리핑은 Claude를 안 부르고 틀에 채운다. 뉴스는 08:00 메일에 이미 있으니 날씨와 오늘 일정만 있으면 된다. 디스코드 브리핑은 웹검색까지 해서 하루 $0.19쯤 들었는데, 이건 0원이다.
좋은 아침이에요. 10/29(목)
맑음, 지금 9°C (최저 6° / 최고 17°, 강수확률 10%)
오늘 일정
· 종일 부모님댁
· 15:00 치과
두뇌는 디스코드와 같은 걸 쓴다
1GB 서버라 Claude 세션을 하나 더 띄울 여유가 없다. 상주 세션 하나가 200MB가 넘는다. 하나 더 두면 아침 메일을 조립하는 동안 메모리가 30MB쯤만 남는다. 그래서 디스코드와 같은 세션을 쓰고 메시지마다 어디서 누가 말했는지 태그를 붙임
[지금: 10/29(목) 21:04]
[카톡·와이프] 토요일 11시 미용실 기록 좀 해줘
페르소나에는 채널별 규칙을 넣었다.
- 디스코드는 나 혼자 쓰니까 반말, 디스코드 마크다운.
- 카톡은 해요체. 카톡은 마크다운을 그리지 않으니까 별표·샵 같은 기호는 금지.
- “ㅋㅋ”, “ㅇㅋ”, “고마워”, 둘이 나누는 대화처럼 답이 필요 없는 말에는 정확히
-한 글자만 답한다. 서버는-가 오면 아무것도 안 보낸다. - 세션을 같이 쓰니까, 디스코드에서 나랑 나눈 대화는 카톡 방에서 꺼내지 않게 막았다. 일정이나 메모처럼 원래 같이 보는 건 괜찮다.
일정 이름 규칙도 하나 정했다. 캘린더 하나를 둘이 같이 보고 있어서 제목 앞에 주인 이름을 붙인다(이름: 치과). 둘이 같이 하는 일정은 이름 없이 둔다(부모님댁). 두 명뿐이라 이름이 없으면 같이 하는 거라는 걸 서로 안다.
맥북을 덮었더니 16분 늦게 답했다
메아리 봇을 켜 둔 채 맥북을 덮고 방에 “노트북 닫아도 보내짐?”이라고 보내 봤다. 답이 16분 뒤에 왔다.
맥북이 잠들면 에뮬레이터도 같이 멈춤.. 멈춘 동안에는 카톡 알림 자체를 못 받고 맥북이 다시 깨어날 때 밀린 알림을 한꺼번에 처리한다. 그러니 맥북은 전원에 꽂아 두고 잠자기 방지를 켜 둬야 한다(시스템 설정 → 배터리 → 옵션). 화면은 꺼져도 괜찮다. 오래 쓸 거면 집에 안 쓰는 안드로이드 폰을 충전기에 꽂아 두는 쪽이 제일 안정적이다.
근데 이렇게 해도 반응이 늦어져서 아예 터미널로 맥북을 sleep하지 못하게 만들었음.
sudo pmset -a disablesleep 1 명령어 실행하면 맥북이 덮든 안덮든 꺼지질 않는다.
사실 나는 맥북이 두갠데 하나는 아예 사용을 안하기 때문에 이 방법을 택했지만,,, 웬만하면 안쓰는 안드로이드 공기계로 이용하자…
일단 위 처럼 조치했는데도 그래도 끊기는 날은 있을 테니, 서버가 알아채게 해 뒀음
- 스크립트의 폴링이 10분 넘게 안 오면 디스코드로 “카톡 연결 끊김”을 보낸다.
- 보낼 목록에서 15분 넘게 못 나간 말(브리핑, 늦은 답)은 디스코드로 대신 보낸다.
디스코드는 이제 대화 창구라기보다 관리자 알림 채널이 된 셈이다.
결과

실제 방에 붙여 보니 잘 된다. 무엇보다 와이프가 카톡에서 바로 시킬 수 있다.
정리
- 카톡 단톡방에 내 봇을 넣는 공식 API는 없다. 알림 기반 메신저봇R이 현실적인 방법이다(비공식이라 언제든 막힐 수 있다는 건 감수).
- 봇 계정 번호는 가상번호 말고 진짜 번호(알뜰폰 eSIM)로. 번호가 사라지면 계정도 같이 사라진다.
- 메신저봇R은 방·사람 고유 ID를 안 준다. 이름으로만 걸러야 하니 봇 계정 ID 검색은 꺼 두자.
- 에뮬레이터를 재부팅하면 방에 메시지가 한 번 와야 먼저 보낼 수 있다.
- 서버와 기기는 Tailscale로 묶고 창구는 Tailscale 주소에만 연다. 서버 키 만료는 꼭 끄자
- 노트북에서 에뮬레이터를 돌리면 잠자기로 돌아가는게 문제긴 함
> 댓글