Slack에서 누가 나를 부르면 「OO님~」이 울리게 만든 이야기
Slack 멘션이 오면 「OO님~」 커스텀 음성이 울리게 만든 자동화. Hammerspoon을 버리고 LaunchAgent로 옮긴 이유.
- Slack
- macOS
- 자동화
- TTS
- 트러블슈팅
목차
Slack에서 @김OO 멘션이 오면 기본 알림음 말고 내가 만든 「OO님~」 음성이 추가로 재생되게 하고 싶었다. Slack 자체 설정만으로는 멘션마다 커스텀 TTS를 넣을 수 없다. 백그라운드에서 Slack을 주기적으로 확인하고 새 멘션이면 소리를 틀어 주는 작은 자동화를 만들었다.
최종 구조를 한 장으로#
로그인하면 macOS LaunchAgent가 자동 실행
↓
3~1초마다 poll-slack.sh 실행
↓
Slack API로 "새 @멘션 있나?" 확인
↓
있으면 afplay로 음성.wav 재생| 구성요소 | 역할 | 비유 |
|---|---|---|
| LaunchAgent | 맥이 켜져 있는 동안 스크립트를 주기적으로 실행 | 알람 시계 |
| poll-slack.sh | Slack API 호출 + 멘션 판별 | 경비원 |
| config.json | 토큰, 내 유저 ID, 감시 채널 목록 | 열쇠랑 설정표 |
| state.json | 마지막으로 본 멘션 시각 | "이미 들은 알림" 메모 |
| 음성.wav | OmniVoice MLX로 만든 TTS | 실제로 울리는 소리 |
파일은 전부 ~/slack-mention/ 한 폴더에 모아 두었다.
Hammerspoon을 뺀 이유#
처음엔 Hammerspoon이라는 맥 자동화 도구를 썼다. Lua 스크립트로 타이머를 돌리고 Slack API를 치고 소리를 내는 방식.
문제가 연달아 터졌다.
- Slack API 호출이 조용히 실패: Hammerspoon 내장 HTTP가 응답을 제대로 못 받는 경우가 있었다. 로그에는 아무것도 안 남고 멘션은 왔는데 소리만 안 남.
- 타이머가 멈춤: 맥이 절전(App Nap)에 들어가면 주기 실행이 끊기기도 했다.
- 299개 채널을 전부 조회: 한때 "모든 대화방 history를 돌자"는 접근을 썼는데 API 호출 한 번에 1초씩 잡으면 한 바퀴 3분. 실시간 알림이 아니라 시험 채점이었다.
Hammerspoon 앱 자체를 uninstall하고 macOS 기본 백그라운드 서비스(LaunchAgent)로 옮겼다. "메뉴바 📣 테스트"는 되는데 Slack 멘션만 안 울리던 패턴. 재생은 되는데 감지 경로만 고장. 여기서 갈라졌다.
Slack 멘션을 어떻게 "감지"하나#
Slack은 멘션 푸시 알림을 주지만 내 커스텀 소리를 같이 트리거해 주진 않는다. API로 직접 확인해야 한다.
방법 A: 검색 API (search.messages)#
"<@내유저ID>" 로 검색. API 호출 1번이라 가볍다.
단점: Slack이 검색 인덱스에 메시지를 올리기까지 10~30초, 가끔 1분 걸린다. "알림은 왔는데 우리 소리는 15초 뒤"가 여기서 나온다. 폴링을 3초로 줄여도 검색 지연은 그대로.
방법 B: 대화 기록 API (conversations.history)#
특정 채널의 최근 메시지 몇 개를 직접 읽는다. 거의 실시간.
단점: 채널마다 API를 한 번씩 쳐야 해서 채널이 많으면 rate limit(호출 횟수 제한)에 걸린다.
최종 타협은 둘 다 쓰되 역할 분담#
| 멘션 위치 | 감지 방식 | 체감 속도 |
|---|---|---|
| watchChannelIds에 넣은 채널 | 매 1초 history 조회 | 1~3초 |
| 나와의 DM | history | 1~3초 |
| 그 외 채널 | search (백업, 30초에 1번) | 10~30초+ |
자주 멘션 받는 채널 ID(C0B23PU8GTG 같은 것)를 config에 넣으면 검색 인덱스 기다릴 필요가 없어진다.
12초 지연이 또 난 이유 (두 번째 함정)#
watch 채널을 넣었는데도 ~12초 걸렸다. 로그를 보니 감지→재생은 2초였다. 진짜 원인: watch 채널을 "매번 확인"이 아니라 활동 중인 채널 6개와 라운드로빈으로 섞어 돌리고 있었다. 7번에 1번만 watch를 보면 7×1초 ≈ 12초. 고친 뒤 watch는 매 폴링마다 무조건 history를 본다.
알림음은 macOS TTS가 아니라 OmniVoice MLX#
첫 시도는 맥 기본 say 명령. 「OO님」이 로봇 같고 한 번은 0.016초짜리 깨진 파일이 나왔다. 바로 버렸다.
OmniVoice MLX는 Apple Silicon 맥에서 로컬로 돌아가는 신경망 TTS다. 인터넷·API 과금 없이 ~/omnivoice-local/에 설치해 둔 상태였다.
최종 생성 명령:
omnivoice-clone "OO님" "" "female, korean accent, moderate pitch"| 인자 | 의미 |
|---|---|
| 1번 | 읽을 텍스트 |
| 2번 | 클론 참조 음성 (빈 문자열 = 없음) |
| 3번 | 목소리 속성 instruct |
Voice Design 모드는 녹음 없이 "여성, 한국어 억양, 중간 톤"만 지정한다. 결과 wav를 ~/slack-mention/sounds/음성.wav에 넣었다.
나중에 래퍼 스크립트(omnivoice-clone)도 손봤다. 원래는 instruct가 있으면 참조 음성을 완전히 무시하는 if/else였는데 모델은 둘 다 받을 수 있다. 참조 음성 + instruct 동시 지원으로 바꿨다. (충돌하면 참조 음성이 이긴다. 남자 녹음에 female instruct면 여자 목소리가 안 나올 수 있다.)
state.json으로 같은 멘션에 소리가 두 번 안 나게#
스크립트를 처음 켜면 Slack에 이미 쌓인 옛 멘션까지 "새 것"으로 오인할 수 있다. 첫 실행은 타임스탬프만 저장하고 소리는 안 낸다.
그다음부터는 state.json의 othersTs, selfDmTs보다 더 최신 멘션만 TRIGGER.
트러블슈팅 요약#
| 증상 | 원인 | 해결 |
|---|---|---|
| 메뉴바 테스트는 되는데 Slack 멘션만 무음 | 재생 OK, 감지 경로 고장 | Hammerspoon → LaunchAgent + curl |
| 15초쯤 늦게 울림 | search API 인덱싱 지연 | watchChannelIds + history |
| watch 넣었는데도 ~12초 | watch가 RR에 섞여 7번에 1번만 조회 | watch는 매 폴링마다 항상 조회 |
| 자기 DM @멘션 테스트 안 됨 | search는 자기 메시지 느림 | self-DM은 history API |
| TTS 퀄리티 형편없음 | macOS say |
OmniVoice MLX |
| Hammerspoon 켜져 있는데 안 됨 | 타이머/App Nap/HTTP | Hammerspoon 제거, LaunchAgent만 |
직접 써보려면#
# 소리만 테스트
bash ~/slack-mention/test-sound.sh
# 폴링 1회
bash ~/slack-mention/poll-slack.sh
# 로그
tail -f ~/slack-mention/poll.logSlack 토큰은 config.json에만 두고 git에는 올리지 않는다.
1초 폴링을 운영하며 남은 기준#
- "알림이 왔다" ≠ "내 스크립트가 안다": Slack 푸시와 API 가시성은 타이밍이 다르다.
- 빠르게 만들려면 search 한 방이 아니라, 중요한 채널만 history로 직접 본다.
- 자동화 도구(Hammerspoon)가 편해도, 백그라운드 신뢰성은 LaunchAgent가 낫다: 이 use case에서는.
- TTS는 기본 내장보다 로컬 MLX 한 번 세팅해 두는 게 낫다: 특히 짧은 커스텀 알림음.
Slack 기본 알림은 그대로 두고 그 위에 「OO님~」이 얹히는 구조다. 과하지 않게, 근데 확실하게. 그게 목표였다.