- Authors
- Name
- 들어가며
- 1. Slack 앱 아키텍처와 설정
- 2. Python Bolt SDK 설치와 기본 구성
- 3. 이벤트 핸들링 -- 메시지, 멘션, 리액션
- 4. 슬래시 커맨드 구현
- 5. 모달과 인터랙티브 컴포넌트
- 6. Block Kit을 활용한 리치 메시지
- 7. Socket Mode vs HTTP Mode 비교
- 8. 봇 프레임워크 비교 -- Slack Bolt vs discord.py vs Telegram Bot
- 9. 배포 전략 -- Docker와 Kubernetes
- 10. 에러 핸들링과 레이트 리밋 관리
- 11. 실패 사례와 복구 절차
- 12. 운영 시 참고사항 (Operational Notes)
- 13. 트러블슈팅 (Troubleshooting)
- 14. 프로덕션 배포 체크리스트
- 마무리
- 참고 자료
들어가며
Slack은 전 세계 수십만 팀이 사용하는 업무 커뮤니케이션 플랫폼입니다. 단순한 메시징을 넘어, Slack Bot을 통해 장애 알림, 배포 자동화, 데이터 조회, 승인 워크플로우 등 다양한 업무를 자동화할 수 있습니다.
Slack 공식 프레임워크인 Bolt for Python은 이벤트 핸들링, 슬래시 커맨드, 모달, Block Kit 등 Slack의 모든 인터랙션을 간결한 데코레이터 패턴으로 구현할 수 있게 해줍니다. 이 글에서는 앱 설정부터 프로덕션 배포, 에러 핸들링, 실패 복구까지 실전에서 필요한 모든 내용을 다룹니다.
1. Slack 앱 아키텍처와 설정
1.1 Slack 앱 아키텍처 개요
Slack Bot의 동작 흐름은 다음과 같습니다.
- 사용자가 Slack에서 메시지, 슬래시 커맨드, 버튼 클릭 등의 인터랙션을 수행
- Slack 서버가 이벤트를 Bot 서버로 전달 (HTTP 또는 WebSocket)
- Bot이 이벤트를 처리하고 Slack Web API를 통해 응답
- 사용자에게 메시지, 모달, 리치 블록 등으로 결과 표시
1.2 앱 생성 및 OAuth 설정
# 1단계: Slack API 사이트에서 앱 생성
# https://api.slack.com/apps 접속
# "Create New App" -> "From scratch" 선택
# App Name: "MySlackBot", Workspace 선택
# 2단계: OAuth & Permissions에서 Bot Token Scopes 설정
# 필요한 스코프 목록:
# chat:write - 메시지 전송
# chat:write.public - 미참여 공개 채널에 메시지 전송
# commands - 슬래시 커맨드 등록
# im:history - DM 메시지 읽기
# im:read - DM 채널 정보 접근
# channels:history - 공개 채널 메시지 읽기
# channels:read - 채널 목록/정보 조회
# users:read - 사용자 프로필 조회
# reactions:read - 리액션 정보 읽기
# reactions:write - 리액션 추가
# files:write - 파일 업로드
# 3단계: Event Subscriptions 활성화
# Subscribe to bot events:
# message.im - DM 수신
# message.channels - 채널 메시지 수신
# app_mention - @봇 멘션 수신
# app_home_opened - 앱 홈 탭 열기
# reaction_added - 리액션 추가 이벤트
# reaction_removed - 리액션 제거 이벤트
# 4단계: Interactivity & Shortcuts 활성화
# 모달, 버튼, 셀렉트 메뉴 등 인터랙티브 기능 사용에 필요
# 5단계: Socket Mode 활성화 (개발/간편 배포용)
# Settings -> Socket Mode -> Enable Socket Mode
# App-Level Token 생성 (connections:write 스코프)
주의사항: Bot Token(xoxb-)과 App Token(xapp-)은 용도가 다릅니다. Bot Token은 Slack Web API 호출에, App Token은 Socket Mode WebSocket 연결에 사용합니다.
1.3 토큰 종류 정리
| 토큰 유형 | 접두사 | 용도 | 획득 경로 |
|---|---|---|---|
| Bot Token | xoxb- | Slack API 호출 (메시지, 채널 등) | OAuth & Permissions |
| App Token | xapp- | Socket Mode WebSocket 연결 | Basic Information |
| User Token | xoxp- | 사용자 권한 기반 API 호출 | OAuth & Permissions |
| Signing Secret | - | HTTP 요청 서명 검증 | Basic Information |
2. Python Bolt SDK 설치와 기본 구성
2.1 프로젝트 초기화
# Python 3.9+ 권장
python -m venv .venv
source .venv/bin/activate
# 핵심 의존성 설치
pip install slack-bolt==1.27.0 slack-sdk python-dotenv
# 프로젝트 디렉토리 구조
mkdir -p slack-bot/{handlers,services,utils}
touch slack-bot/{app.py,.env,requirements.txt}
touch slack-bot/handlers/{__init__.py,commands.py,events.py,actions.py,modals.py}
touch slack-bot/services/{__init__.py,notification.py}
touch slack-bot/utils/{__init__.py,rate_limiter.py,logger.py}
# 디렉토리 구조 확인
# slack-bot/
# ├── app.py # 메인 앱 엔트리포인트
# ├── .env # 환경변수 (토큰 등)
# ├── requirements.txt
# ├── Dockerfile
# ├── handlers/
# │ ├── __init__.py
# │ ├── commands.py # 슬래시 커맨드 핸들러
# │ ├── events.py # 이벤트 핸들러 (메시지, 멘션)
# │ ├── actions.py # 버튼/셀렉트 액션 핸들러
# │ └── modals.py # 모달 뷰 핸들러
# ├── services/
# │ ├── __init__.py
# │ └── notification.py # 알림 서비스
# └── utils/
# ├── __init__.py
# ├── rate_limiter.py # 레이트 리밋 유틸리티
# └── logger.py # 로깅 설정
2.2 메인 앱 기본 구성
# app.py
import os
import logging
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
load_dotenv()
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
# Bolt App 초기화
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
)
# ===== 글로벌 미들웨어 =====
@app.middleware
def log_request(logger, body, next):
"""모든 요청을 로깅하는 미들웨어"""
logger.info(f"Request type: {body.get('type', 'unknown')}")
next()
# ===== 이벤트 핸들러 등록 =====
from handlers.events import register_event_handlers
from handlers.commands import register_command_handlers
from handlers.actions import register_action_handlers
from handlers.modals import register_modal_handlers
register_event_handlers(app)
register_command_handlers(app)
register_action_handlers(app)
register_modal_handlers(app)
# ===== 글로벌 에러 핸들러 =====
@app.error
def global_error_handler(error, body, logger):
logger.exception(f"Unhandled error: {error}")
logger.debug(f"Request body: {body}")
# ===== 실행 =====
if __name__ == "__main__":
logger.info("Slack Bot starting in Socket Mode...")
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
3. 이벤트 핸들링 -- 메시지, 멘션, 리액션
Slack Bot의 핵심은 이벤트 처리입니다. Bolt SDK는 데코레이터 패턴으로 이벤트 리스너를 등록합니다.
3.1 이벤트 핸들러 구현
# handlers/events.py
import re
from datetime import datetime
def register_event_handlers(app):
"""이벤트 핸들러를 앱에 등록"""
# --- 앱 멘션 처리 ---
@app.event("app_mention")
def handle_app_mention(event, say, client, logger):
"""@봇 멘션 시 호출"""
user_id = event["user"]
text = event.get("text", "")
channel = event["channel"]
# 멘션 텍스트에서 봇 ID 제거
clean_text = re.sub(r"<@\w+>", "", text).strip()
try:
user_info = client.users_info(user=user_id)
display_name = user_info["user"]["real_name"]
except Exception as e:
logger.error(f"Failed to fetch user info: {e}")
display_name = "사용자"
if "도움" in clean_text or "help" in clean_text.lower():
say(
channel=channel,
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"안녕하세요 {display_name}님!\n\n"
"*사용 가능한 명령어:*\n"
"- `/task` - 작업 생성\n"
"- `/status` - 서비스 상태 조회\n"
"- `/oncall` - 온콜 담당자 확인\n"
"- DM으로 자유롭게 질문하세요!"
),
},
}
],
)
else:
say(
channel=channel,
text=f"{display_name}님, 말씀하신 내용을 확인하겠습니다.",
thread_ts=event.get("ts"), # 스레드로 응답
)
# --- DM 메시지 처리 ---
@app.event("message")
def handle_message(event, say, logger):
"""채널/DM 메시지 이벤트 처리"""
# 봇 자신의 메시지, 편집, 삭제 이벤트 무시
if event.get("bot_id") or event.get("subtype"):
return
channel_type = event.get("channel_type", "")
text = event.get("text", "")
user_id = event.get("user", "")
# DM 메시지만 자동 응답
if channel_type == "im":
logger.info(f"DM from {user_id}: {text[:50]}...")
# 키워드 기반 분기 처리
if "장애" in text or "incident" in text.lower():
say(
text="장애 보고를 등록하시겠습니까?",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "장애 보고를 등록하시겠습니까?",
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "장애 보고"},
"action_id": "open_incident_modal",
"style": "danger",
},
}
],
)
else:
say(f"메시지를 받았습니다. 자세한 도움이 필요하시면 `/help`를 입력해주세요.")
# --- 리액션 이벤트 처리 ---
@app.event("reaction_added")
def handle_reaction_added(event, client, logger):
"""리액션 추가 이벤트 처리 (예: 이모지로 작업 트리거)"""
reaction = event["reaction"]
item = event["item"]
user_id = event["user"]
logger.info(f"Reaction :{reaction}: added by {user_id}")
# :white_check_mark: 리액션으로 작업 완료 처리
if reaction == "white_check_mark":
client.chat_postMessage(
channel=item["channel"],
thread_ts=item["ts"],
text=f"<@{user_id}>님이 이 작업을 완료 처리했습니다.",
)
# :eyes: 리액션으로 작업 확인 처리
elif reaction == "eyes":
client.chat_postMessage(
channel=item["channel"],
thread_ts=item["ts"],
text=f"<@{user_id}>님이 이 내용을 확인 중입니다.",
)
# --- 앱 홈 탭 ---
@app.event("app_home_opened")
def handle_app_home_opened(client, event, logger):
"""앱 홈 탭 열릴 때 대시보드 표시"""
user_id = event["user"]
now = datetime.now().strftime("%Y-%m-%d %H:%M")
try:
client.views_publish(
user_id=user_id,
view={
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Bot Dashboard",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
"*사용 가능한 기능:*\n"
"- `/task` : 작업 생성 및 관리\n"
"- `/status` : 서비스 상태 확인\n"
"- `/oncall` : 온콜 담당자 조회\n"
"- DM : 자유 질의응답"
),
},
},
{"type": "divider"},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"마지막 업데이트: {now}",
}
],
},
],
},
)
except Exception as e:
logger.error(f"Failed to publish home tab: {e}")
핵심 포인트:
event.get("bot_id")와event.get("subtype")체크는 무한 루프 방지에 필수입니다.thread_ts를 지정하면 스레드에 응답하여 채널이 깔끔하게 유지됩니다.- 리액션 이벤트를 활용하면 이모지 기반의 간편한 워크플로우를 만들 수 있습니다.
4. 슬래시 커맨드 구현
슬래시 커맨드는 사용자가 /명령어를 입력하여 봇 기능을 호출하는 방식입니다.
4.1 기본 커맨드와 모달 연동
# handlers/commands.py
import json
from datetime import datetime
def register_command_handlers(app):
@app.command("/task")
def handle_task_command(ack, body, client, logger):
"""작업 생성 모달을 여는 슬래시 커맨드"""
# 반드시 3초 내에 ack() 호출 필수!
ack()
try:
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "task_create_modal",
"title": {"type": "plain_text", "text": "작업 생성"},
"submit": {"type": "plain_text", "text": "생성"},
"close": {"type": "plain_text", "text": "취소"},
"blocks": [
{
"type": "input",
"block_id": "title_block",
"element": {
"type": "plain_text_input",
"action_id": "title_input",
"placeholder": {
"type": "plain_text",
"text": "작업 제목을 입력하세요",
},
},
"label": {"type": "plain_text", "text": "제목"},
},
{
"type": "input",
"block_id": "priority_block",
"element": {
"type": "static_select",
"action_id": "priority_select",
"options": [
{
"text": {"type": "plain_text", "text": "P1 - 긴급"},
"value": "P1",
},
{
"text": {"type": "plain_text", "text": "P2 - 높음"},
"value": "P2",
},
{
"text": {"type": "plain_text", "text": "P3 - 보통"},
"value": "P3",
},
{
"text": {"type": "plain_text", "text": "P4 - 낮음"},
"value": "P4",
},
],
},
"label": {"type": "plain_text", "text": "우선순위"},
},
{
"type": "input",
"block_id": "assignee_block",
"element": {
"type": "users_select",
"action_id": "assignee_select",
"placeholder": {
"type": "plain_text",
"text": "담당자 선택",
},
},
"label": {"type": "plain_text", "text": "담당자"},
},
{
"type": "input",
"block_id": "desc_block",
"element": {
"type": "plain_text_input",
"action_id": "desc_input",
"multiline": True,
"placeholder": {
"type": "plain_text",
"text": "작업 내용을 상세히 기술하세요",
},
},
"label": {"type": "plain_text", "text": "설명"},
"optional": True,
},
],
},
)
except Exception as e:
logger.error(f"Failed to open modal: {e}")
@app.command("/status")
def handle_status_command(ack, say, command, logger):
"""서비스 상태를 조회하는 커맨드"""
ack()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
say(
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "Service Status Dashboard"},
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*API Server*\nHealthy (v2.4.1)"},
{"type": "mrkdwn", "text": "*Web Frontend*\nHealthy (v3.1.0)"},
{"type": "mrkdwn", "text": "*Worker*\nDegraded (v1.8.2)"},
{"type": "mrkdwn", "text": "*Database*\nHealthy (CPU 38%)"},
],
},
{"type": "divider"},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "상세 보기"},
"action_id": "status_detail",
"value": "all",
},
{
"type": "button",
"text": {"type": "plain_text", "text": "새로고침"},
"action_id": "status_refresh",
},
],
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"조회 시각: {now}",
}
],
},
],
)
@app.command("/oncall")
def handle_oncall_command(ack, say, command, logger):
"""현재 온콜 담당자를 조회하는 커맨드"""
ack()
# 실제로는 PagerDuty, OpsGenie 등 외부 서비스 연동
say(
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
"*현재 온콜 담당자*\n\n"
"- Backend: <@U12345678>\n"
"- Frontend: <@U23456789>\n"
"- Infra: <@U34567890>\n\n"
"긴급 연락은 온콜 담당자에게 DM을 보내주세요."
),
},
}
],
)
ack() 필수 호출: Slack은 슬래시 커맨드를 보낸 후 3초 이내에 acknowledge 응답을 기대합니다. ack()를 호출하지 않으면 사용자에게 "이 슬래시 커맨드를 처리하지 못했습니다"라는 에러가 표시됩니다.
5. 모달과 인터랙티브 컴포넌트
5.1 모달 제출 처리
# handlers/modals.py
from datetime import datetime
def register_modal_handlers(app):
@app.view("task_create_modal")
def handle_task_modal_submission(ack, body, client, view, logger):
"""작업 생성 모달 제출 처리"""
# 입력값 검증
values = view["state"]["values"]
title = values["title_block"]["title_input"]["value"]
errors = {}
if len(title) < 3:
errors["title_block"] = "제목은 최소 3자 이상이어야 합니다."
if errors:
ack(response_action="errors", errors=errors)
return
ack()
# 입력값 추출
priority = values["priority_block"]["priority_select"]["selected_option"]["value"]
assignee = values["assignee_block"]["assignee_select"]["selected_user"]
description = values["desc_block"]["desc_input"].get("value", "설명 없음")
creator = body["user"]["id"]
# 알림 채널에 작업 생성 메시지 전송
now = datetime.now().strftime("%Y-%m-%d %H:%M")
client.chat_postMessage(
channel="#tasks",
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "새 작업이 생성되었습니다"},
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*제목:*\n{title}"},
{"type": "mrkdwn", "text": f"*우선순위:*\n{priority}"},
{"type": "mrkdwn", "text": f"*담당자:*\n<@{assignee}>"},
{"type": "mrkdwn", "text": f"*생성자:*\n<@{creator}>"},
],
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*설명:*\n{description}"},
},
{"type": "divider"},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "작업 시작"},
"style": "primary",
"action_id": "task_start",
"value": title,
},
{
"type": "button",
"text": {"type": "plain_text", "text": "완료"},
"action_id": "task_complete",
"value": title,
},
],
},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": f"생성 시각: {now}"}
],
},
],
)
# 담당자에게 DM 알림
client.chat_postMessage(
channel=assignee,
text=f"새 작업이 할당되었습니다: *{title}* (우선순위: {priority})\n생성자: <@{creator}>",
)
5.2 버튼 액션 처리
# handlers/actions.py
def register_action_handlers(app):
@app.action("task_start")
def handle_task_start(ack, body, client, action, logger):
"""작업 시작 버튼 클릭 처리"""
ack()
user_id = body["user"]["id"]
task_title = action["value"]
# 원본 메시지 업데이트
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*{task_title}*\n"
f"상태: 진행 중\n"
f"담당: <@{user_id}>"
),
},
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "완료 처리"},
"style": "primary",
"action_id": "task_complete",
"value": task_title,
},
],
},
],
)
@app.action("task_complete")
def handle_task_complete(ack, body, client, action, logger):
"""작업 완료 버튼 클릭 처리"""
ack()
user_id = body["user"]["id"]
task_title = action["value"]
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"~{task_title}~\n"
f"상태: 완료\n"
f"완료자: <@{user_id}>"
),
},
},
],
)
@app.action("open_incident_modal")
def handle_open_incident(ack, body, client, logger):
"""장애 보고 모달 열기"""
ack()
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "incident_report_modal",
"title": {"type": "plain_text", "text": "장애 보고"},
"submit": {"type": "plain_text", "text": "보고"},
"blocks": [
{
"type": "input",
"block_id": "severity_block",
"element": {
"type": "static_select",
"action_id": "severity_select",
"options": [
{
"text": {"type": "plain_text", "text": "P1 - Critical"},
"value": "P1",
},
{
"text": {"type": "plain_text", "text": "P2 - Major"},
"value": "P2",
},
{
"text": {"type": "plain_text", "text": "P3 - Minor"},
"value": "P3",
},
],
},
"label": {"type": "plain_text", "text": "심각도"},
},
{
"type": "input",
"block_id": "incident_desc_block",
"element": {
"type": "plain_text_input",
"action_id": "incident_desc_input",
"multiline": True,
"placeholder": {
"type": "plain_text",
"text": "장애 상황을 상세히 기술하세요",
},
},
"label": {"type": "plain_text", "text": "상황 설명"},
},
],
},
)
@app.action("status_refresh")
def handle_status_refresh(ack, body, logger):
"""상태 새로고침 버튼"""
ack()
logger.info("Status refresh requested")
@app.action("status_detail")
def handle_status_detail(ack, body, logger):
"""상태 상세보기 버튼"""
ack()
logger.info("Status detail requested")
6. Block Kit을 활용한 리치 메시지
Block Kit은 Slack의 UI 프레임워크로, 구조화된 인터랙티브 메시지를 구성합니다. 주요 블록 타입은 다음과 같습니다.
| 블록 타입 | 용도 | 사용 위치 |
|---|---|---|
section | 텍스트, 필드, 액세서리(버튼 등) | 메시지, 모달 |
actions | 버튼, 셀렉트 메뉴, 데이트피커 등 | 메시지 |
input | 사용자 입력 (텍스트, 셀렉트) | 모달 전용 |
header | 큰 텍스트 제목 | 메시지, 모달 |
divider | 구분선 | 메시지, 모달 |
context | 작은 텍스트, 이미지 | 메시지, 모달 |
image | 이미지 표시 | 메시지, 모달 |
rich_text | 서식 있는 텍스트 | 메시지 |
Block Kit Builder(https://app.slack.com/block-kit-builder)를 사용하면 시각적으로 블록을 구성하고 JSON을 미리볼 수 있습니다.
7. Socket Mode vs HTTP Mode 비교
| 항목 | Socket Mode | HTTP Mode |
|---|---|---|
| 연결 방식 | WebSocket (양방향) | HTTP POST (단방향) |
| 공개 URL 필요 | 불필요 | 필요 (HTTPS 엔드포인트) |
| 방화벽 | 방화벽 뒤에서도 동작 | 외부 접근 가능해야 함 |
| 토큰 | App Token (xapp-) + Bot Token | Bot Token + Signing Secret |
| 로컬 개발 | 매우 편리 (ngrok 불필요) | ngrok 등 터널링 도구 필요 |
| 확장성 | 단일 인스턴스 권장 | 다중 인스턴스 로드밸런싱 가능 |
| 안정성 | 장시간 연결 시 재연결 필요 | Stateless, 안정적 |
| 권장 환경 | 내부 도구, 소규모 팀 | 대규모 서비스, Marketplace 앱 |
| Bolt 설정 | SocketModeHandler | App() + WSGI/ASGI |
Socket Mode 실행 코드
# Socket Mode (개발, 내부 도구)
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=os.environ["SLACK_BOT_TOKEN"])
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
HTTP Mode 실행 코드 (Flask)
# HTTP Mode (프로덕션, 외부 서비스)
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
)
flask_app = Flask(__name__)
handler = SlackRequestHandler(app)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
@flask_app.route("/slack/commands", methods=["POST"])
def slack_commands():
return handler.handle(request)
@flask_app.route("/slack/interactions", methods=["POST"])
def slack_interactions():
return handler.handle(request)
if __name__ == "__main__":
flask_app.run(port=3000)
선택 기준: 개발 단계나 내부 전용 봇이라면 Socket Mode가 편리합니다. 다수 워크스페이스에 배포하거나 Marketplace에 등록할 예정이라면 HTTP Mode를 사용하세요.
8. 봇 프레임워크 비교 -- Slack Bolt vs discord.py vs Telegram Bot
| 항목 | Slack Bolt (Python) | discord.py | python-telegram-bot |
|---|---|---|---|
| 플랫폼 | Slack | Discord | Telegram |
| 설치 | pip install slack-bolt | pip install discord.py | pip install python-telegram-bot |
| 인증 방식 | OAuth 2.0 + Bot Token | Bot Token | Bot Token (BotFather) |
| 이벤트 수신 | Socket Mode / HTTP | Gateway (WebSocket) | Polling / Webhook |
| UI 프레임워크 | Block Kit (리치 블록) | Embed, Button, Select | InlineKeyboard, ReplyKeyboard |
| 모달 지원 | 지원 (views_open) | Modal (discord.ui) | 미지원 (대화형 흐름으로 대체) |
| 슬래시 커맨드 | 네이티브 지원 | Application Command | BotCommand |
| 파일 업로드 | files.upload API | File 객체 | send_document |
| 레이트 리밋 | Tier별 (1~100+ req/min) | 50 req/sec (글로벌) | 30 msg/sec (그룹), 1/sec (개인) |
| 비동기 지원 | AsyncApp 제공 | 기본 비동기 | asyncio 기본 |
| 문서 품질 | 우수 (공식 가이드 충실) | 우수 (커뮤니티 활발) | 우수 (예제 풍부) |
| 엔터프라이즈 | Slack Enterprise Grid | 제한적 | 제한적 |
9. 배포 전략 -- Docker와 Kubernetes
9.1 Dockerfile
FROM python:3.12-slim
WORKDIR /app
# 의존성 먼저 복사 (레이어 캐싱 활용)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 애플리케이션 코드 복사
COPY . .
# 비루트 사용자로 실행
RUN adduser --disabled-password --gecos "" botuser
USER botuser
# 헬스체크 (Socket Mode에서는 별도 HTTP 서버 필요)
HEALTHCHECK \
CMD python -c "print('healthy')" || exit 1
CMD ["python", "app.py"]
9.2 Docker Compose
# docker-compose.yml
services:
slack-bot:
build: .
env_file: .env
restart: unless-stopped
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'
9.3 Kubernetes Deployment
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: slack-bot
labels:
app: slack-bot
spec:
replicas: 1 # Socket Mode는 단일 인스턴스 권장
selector:
matchLabels:
app: slack-bot
template:
metadata:
labels:
app: slack-bot
spec:
containers:
- name: slack-bot
image: myregistry/slack-bot:latest
resources:
requests:
memory: '128Mi'
cpu: '100m'
limits:
memory: '256Mi'
cpu: '500m'
envFrom:
- secretRef:
name: slack-bot-secrets
livenessProbe:
exec:
command:
- python
- -c
- "print('alive')"
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
exec:
command:
- python
- -c
- "print('ready')"
initialDelaySeconds: 5
periodSeconds: 10
restartPolicy: Always
---
apiVersion: v1
kind: Secret
metadata:
name: slack-bot-secrets
type: Opaque
stringData:
SLACK_BOT_TOKEN: 'xoxb-your-bot-token'
SLACK_APP_TOKEN: 'xapp-your-app-token'
SLACK_SIGNING_SECRET: 'your-signing-secret'
Socket Mode 배포 시 주의: Socket Mode는 하나의 WebSocket 연결만 유지하므로 replicas: 1로 설정합니다. 고가용성이 필요하면 HTTP Mode로 전환하고 로드밸런서를 구성하세요.
10. 에러 핸들링과 레이트 리밋 관리
10.1 레이트 리밋 처리
Slack API는 메서드별로 레이트 리밋 Tier가 다릅니다.
| Tier | 허용량 | 대표 메서드 |
|---|---|---|
| Tier 1 | 1 req/min | admin.* |
| Tier 2 | 20 req/min | conversations.create |
| Tier 3 | 50 req/min | chat.postMessage |
| Tier 4 | 100+ req/min | users.info |
| Special | 1 req/sec (burst) | chat.postMessage (워크스페이스당) |
# utils/rate_limiter.py
import time
import logging
from slack_sdk.errors import SlackApiError
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
logger = logging.getLogger(__name__)
def configure_rate_limit_handler(client):
"""WebClient에 레이트 리밋 핸들러 추가"""
rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=3)
client.retry_handlers.append(rate_limit_handler)
return client
def safe_api_call(func, *args, max_retries=3, **kwargs):
"""레이트 리밋을 고려한 안전한 API 호출 래퍼"""
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except SlackApiError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 5))
logger.warning(
f"Rate limited. Retrying after {retry_after}s "
f"(attempt {attempt + 1}/{max_retries})"
)
time.sleep(retry_after)
else:
logger.error(f"Slack API error: {e.response['error']}")
raise
raise Exception(f"Max retries ({max_retries}) exceeded for API call")
# 사용 예시
# result = safe_api_call(
# client.chat_postMessage,
# channel="#general",
# text="Hello!"
# )
10.2 글로벌 에러 핸들링 패턴
# 앱 수준 에러 핸들러
@app.error
def global_error_handler(error, body, logger):
"""앱 전체의 미처리 에러를 캐치"""
logger.exception(f"Unhandled error: {error}")
# 에러 유형별 분기 처리
if isinstance(error, SlackApiError):
error_code = error.response.get("error", "unknown_error")
logger.error(f"Slack API Error: {error_code}")
if error_code == "channel_not_found":
logger.warning("Channel not found - check channel ID or bot permissions")
elif error_code == "not_in_channel":
logger.warning("Bot is not in the channel - invite the bot first")
elif error_code == "token_revoked":
logger.critical("Bot token has been revoked!")
else:
logger.error(f"Non-Slack error: {type(error).__name__}: {error}")
11. 실패 사례와 복구 절차
11.1 흔한 실패 사례
사례 1: ack() 미호출로 인한 타임아웃
- 증상: 슬래시 커맨드 실행 시 "operation_timeout" 에러 표시
- 원인: 핸들러 시작 시
ack()를 호출하지 않거나, 시간이 오래 걸리는 작업을ack()전에 수행 - 해결: 반드시 핸들러 첫 줄에서
ack()호출. 오래 걸리는 작업은ack()후 별도로 처리
사례 2: Socket Mode 연결 끊김
- 증상: 봇이 갑자기 응답을 멈춤
- 원인: 네트워크 불안정, 서버 재시작, Slack 측 WebSocket 세션 만료
- 해결:
SocketModeHandler는 자동 재연결을 지원하지만, 프로세스 감시자(systemd, supervisor) 또는 Kubernetes의 restartPolicy로 이중 보호
사례 3: 봇 메시지 무한 루프
- 증상: 봇이 자기 메시지에 반응하여 무한 메시지 전송
- 원인:
message이벤트 핸들러에서bot_id체크 누락 - 해결:
if event.get("bot_id"): return가드 코드 추가
사례 4: 모달 입력값 검증 실패
- 증상: 모달 제출 시 아무 반응 없음
- 원인:
ack()호출 시response_action="errors"없이 에러 반환 - 해결: 검증 실패 시
ack(response_action="errors", errors={...})패턴 사용
사례 5: 토큰 만료 또는 스코프 부족
- 증상:
missing_scope또는invalid_auth에러 - 원인: 필요한 OAuth 스코프를 추가하지 않았거나 토큰 재발급 미수행
- 해결: OAuth & Permissions에서 스코프 추가 후 앱 재설치
11.2 복구 절차 체크리스트
장애 발생 시 다음 순서로 복구를 진행합니다.
- 로그 확인:
docker logs slack-bot또는kubectl logs명령으로 에러 메시지 확인 - 토큰 유효성 검증:
curl -H "Authorization: Bearer xoxb-..." https://slack.com/api/auth.test호출 - 이벤트 구독 상태 확인: Slack 앱 설정 페이지에서 Event Subscriptions 상태 점검
- 네트워크 연결 확인: Socket Mode의 경우 WebSocket 연결 상태, HTTP Mode의 경우 엔드포인트 접근 가능 여부
- 프로세스 재시작:
docker restart slack-bot또는kubectl rollout restart deployment/slack-bot - 스코프 재확인: 에러 메시지에
missing_scope가 있다면 해당 스코프를 추가하고 앱 재설치
12. 운영 시 참고사항 (Operational Notes)
로깅 권장사항
- 모든 이벤트 핸들러에 로깅을 추가하여 디버깅 편의성을 확보하세요.
- 민감 정보(토큰, 사용자 메시지 전문)는 로그에 남기지 마세요.
- 구조화된 로깅(JSON format)을 사용하면 로그 분석 도구와 연동이 쉽습니다.
보안 주의사항
.env파일은 절대 Git에 커밋하지 마세요..gitignore에 반드시 추가합니다.- HTTP Mode 사용 시
signing_secret으로 요청 서명을 반드시 검증하세요 (Bolt SDK가 자동 처리). - Bot Token 권한은 최소 권한 원칙(Principle of Least Privilege)을 따르세요.
성능 최적화
- 오래 걸리는 작업(외부 API 호출, DB 조회 등)은
ack()후 별도 스레드에서 처리하세요. AsyncApp을 사용하면 비동기 이벤트 처리로 처리량을 높일 수 있습니다.- Block Kit 메시지의 블록 수는 최대 50개까지 허용되므로, 긴 콘텐츠는 페이징 처리하세요.
13. 트러블슈팅 (Troubleshooting)
| 증상 | 원인 | 해결책 |
|---|---|---|
| "dispatch_failed" 에러 | 이벤트 핸들러가 등록되지 않음 | @app.event("message") 등 핸들러 등록 확인 |
| 슬래시 커맨드 무응답 | ack() 미호출 또는 3초 초과 | 핸들러 첫 줄에 ack() 추가 |
| "not_authed" 에러 | 토큰이 비어 있거나 잘못됨 | .env 파일과 환경변수 확인 |
| 모달이 열리지 않음 | trigger_id 만료 (3초) | 모달 열기를 ack() 직후에 실행 |
| 리액션 이벤트 미수신 | Event Subscription 미설정 | reaction_added 이벤트 구독 추가 |
| "missing_scope" 에러 | 필요한 OAuth 스코프 누락 | OAuth & Permissions에서 스코프 추가 후 재설치 |
| Socket Mode 연결 실패 | App Token 미생성 또는 잘못됨 | Basic Information에서 App Token 재생성 |
| 봇이 채널에 메시지 못 보냄 | 봇이 해당 채널에 미참여 | 채널에 봇을 초대하거나 chat:write.public 스코프 추가 |
| HTTP 429 응답 | 레이트 리밋 초과 | Retry-After 헤더 값만큼 대기 후 재시도 |
| 이벤트 중복 수신 | Slack의 재전송 메커니즘 | 이벤트 ID 기반 중복 제거(idempotency) 로직 추가 |
14. 프로덕션 배포 체크리스트
배포 전 다음 항목을 모두 확인하세요.
- Bot Token(
xoxb-)과 App Token(xapp-)이 올바르게 설정되었는지 확인 - 필요한 OAuth 스코프가 모두 추가되었는지 확인
- Event Subscriptions에서 필요한 이벤트가 모두 구독되었는지 확인
- Interactivity가 활성화되었는지 확인
- 슬래시 커맨드가 Slack 앱 설정에 등록되었는지 확인
.env파일이.gitignore에 추가되었는지 확인- 모든 핸들러에서
ack()가 호출되는지 확인 - 봇 자신의 메시지에 대한 무한루프 방지 로직이 있는지 확인
- 레이트 리밋 핸들링이 구현되었는지 확인
- 에러 핸들러(
@app.error)가 등록되었는지 확인 - 로깅이 적절히 설정되었는지 확인
- Docker 이미지가 비루트 사용자로 실행되는지 확인
- Kubernetes Secret으로 토큰이 관리되는지 확인
- 프로세스 재시작 정책(restart policy)이 설정되었는지 확인
- Socket Mode 사용 시 replicas가 1인지 확인
마무리
Slack Bot은 팀의 업무 효율을 획기적으로 높이는 도구입니다. Python Bolt SDK를 사용하면 이벤트 핸들링, 슬래시 커맨드, 모달, Block Kit 등 Slack의 모든 인터랙션을 간결한 코드로 구현할 수 있습니다.
핵심 정리:
- ack()는 모든 인터랙션 핸들러의 시작점입니다. 3초 타임아웃을 항상 기억하세요.
- Socket Mode는 개발과 내부 도구에 최적이고, HTTP Mode는 프로덕션 규모 서비스에 적합합니다.
- Block Kit으로 리치 메시지를 구성하고, 모달로 사용자 입력을 구조적으로 받으세요.
- 레이트 리밋 핸들링과 에러 복구 절차를 반드시 구현하세요.
- Docker와 Kubernetes로 안정적인 프로덕션 배포 환경을 구축하세요.