- Published on
Slack Bot 개발 완벽 가이드 — Bolt SDK로 만드는 AI 업무 자동화 봇
- Authors
- Name

들어가며
Slack은 개발팀의 핵심 커뮤니케이션 플랫폼입니다. 잘 만든 Slack Bot은 장애 알림, 배포 관리, 코드 리뷰 리마인더, AI 질의 등 다양한 업무를 자동화합니다.
Bolt for Python은 Slack의 공식 프레임워크로, 간결한 API로 다양한 Slack 기능을 활용할 수 있습니다.
Slack 앱 설정
1. 앱 생성
1. https://api.slack.com/apps 접속
2. "Create New App" → "From scratch"
3. App Name: "DevOps Bot"
4. Workspace 선택
2. Bot Token Scopes 설정
OAuth & Permissions → Bot Token Scopes:
- chat:write (메시지 전송)
- commands (슬래시 명령어)
- im:history (DM 기록 읽기)
- im:read (DM 채널 정보)
- channels:history (채널 메시지 읽기)
- channels:read (채널 정보)
- users:read (사용자 정보)
- reactions:write (리액션 추가)
- files:write (파일 업로드)
3. Event Subscriptions
Event Subscriptions → Enable Events
Subscribe to bot events:
- message.im (DM 수신)
- message.channels (채널 메시지)
- app_mention (멘션)
- app_home_opened (앱 홈 탭 열기)
프로젝트 설정
# 의존성 설치
pip install slack-bolt slack-sdk python-dotenv openai
# 프로젝트 구조
slack-bot/
├── app.py # 메인 앱
├── handlers/
│ ├── __init__.py
│ ├── commands.py # 슬래시 명령어
│ ├── events.py # 이벤트 핸들러
│ ├── actions.py # 버튼/액션 핸들러
│ └── modals.py # 모달 뷰
├── services/
│ ├── __init__.py
│ ├── ai.py # LLM 통합
│ ├── deploy.py # 배포 서비스
│ └── github.py # GitHub 연동
├── .env
└── requirements.txt
# .env
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
OPENAI_API_KEY=sk-your-key
기본 앱 구성
# app.py
import os
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
load_dotenv()
# Socket Mode로 앱 생성 (웹훅 서버 불필요)
app = App(token=os.environ["SLACK_BOT_TOKEN"])
# ===== 이벤트 핸들러 =====
# 멘션에 응답
@app.event("app_mention")
def handle_mention(event, say, client):
user = event["user"]
text = event["text"]
# 유저 정보 조회
user_info = client.users_info(user=user)
name = user_info["user"]["real_name"]
say(f"안녕하세요 {name}님! 무엇을 도와드릴까요? 🤖")
# DM 메시지 처리
@app.event("message")
def handle_dm(event, say, client, logger):
# 봇 자신의 메시지는 무시
if event.get("bot_id"):
return
channel_type = event.get("channel_type")
if channel_type == "im":
text = event.get("text", "")
say(f"말씀하신 내용을 처리하겠습니다: '{text}'")
# ===== 슬래시 명령어 =====
@app.command("/deploy")
def handle_deploy_command(ack, body, client):
ack() # 3초 내 응답 필수!
# 모달 열기
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "deploy_modal",
"title": {"type": "plain_text", "text": "배포 관리"},
"submit": {"type": "plain_text", "text": "배포 시작"},
"blocks": [
{
"type": "input",
"block_id": "service_block",
"element": {
"type": "static_select",
"placeholder": {"type": "plain_text", "text": "서비스 선택"},
"options": [
{"text": {"type": "plain_text", "text": "API Server"}, "value": "api"},
{"text": {"type": "plain_text", "text": "Web Frontend"}, "value": "web"},
{"text": {"type": "plain_text", "text": "Worker"}, "value": "worker"},
],
"action_id": "service_select",
},
"label": {"type": "plain_text", "text": "서비스"},
},
{
"type": "input",
"block_id": "env_block",
"element": {
"type": "static_select",
"options": [
{"text": {"type": "plain_text", "text": "Staging"}, "value": "staging"},
{"text": {"type": "plain_text", "text": "Production"}, "value": "production"},
],
"action_id": "env_select",
},
"label": {"type": "plain_text", "text": "환경"},
},
{
"type": "input",
"block_id": "version_block",
"element": {
"type": "plain_text_input",
"placeholder": {"type": "plain_text", "text": "v1.2.3 또는 latest"},
"action_id": "version_input",
},
"label": {"type": "plain_text", "text": "버전"},
},
],
},
)
# 모달 제출 처리
@app.view("deploy_modal")
def handle_deploy_submission(ack, body, client, view):
ack()
values = view["state"]["values"]
service = values["service_block"]["service_select"]["selected_option"]["value"]
env = values["env_block"]["env_select"]["selected_option"]["value"]
version = values["version_block"]["version_input"]["value"]
user_id = body["user"]["id"]
# 배포 알림 전송
client.chat_postMessage(
channel="#deployments",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"🚀 *배포 시작*\n"
f"• 서비스: `{service}`\n"
f"• 환경: `{env}`\n"
f"• 버전: `{version}`\n"
f"• 요청자: <@{user_id}>"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "✅ 승인"},
"style": "primary",
"value": f"{service}|{env}|{version}",
"action_id": "deploy_approve",
},
{
"type": "button",
"text": {"type": "plain_text", "text": "❌ 거부"},
"style": "danger",
"value": f"{service}|{env}|{version}",
"action_id": "deploy_reject",
},
],
},
],
)
# 버튼 액션 처리
@app.action("deploy_approve")
def handle_approve(ack, body, client, action):
ack()
service, env, version = action["value"].split("|")
user_id = body["user"]["id"]
# 원래 메시지 업데이트
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"✅ *배포 승인됨*\n"
f"• 서비스: `{service}` → `{env}` ({version})\n"
f"• 승인자: <@{user_id}>\n"
f"• 상태: 배포 진행 중... ⏳"
}
}
],
)
# 실제 배포 로직 실행
# deploy_service(service, env, version)
# ===== 상태 조회 명령어 =====
@app.command("/status")
def handle_status(ack, say, command):
ack()
say(
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "📊 서비스 상태"}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*API Server*\n🟢 Healthy (v1.2.3)"},
{"type": "mrkdwn", "text": "*Web Frontend*\n🟢 Healthy (v2.0.1)"},
{"type": "mrkdwn", "text": "*Worker*\n🟡 Degraded (v1.1.0)"},
{"type": "mrkdwn", "text": "*Database*\n🟢 Healthy (CPU: 45%)"},
]
},
{"type": "divider"},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": f"마지막 업데이트: <!date^{int(__import__('time').time())}^{{date_short_pretty}} {{time}}|just now>"}
]
}
]
)
# ===== AI 챗봇 기능 =====
@app.command("/ask")
def handle_ask(ack, say, command):
ack()
question = command["text"]
if not question:
say("질문을 입력해주세요. 예: `/ask Kubernetes Pod이 CrashLoopBackOff일 때 대처법`")
return
# 로딩 표시
say(f"🤔 '{question}'에 대해 생각하고 있습니다...")
# LLM 호출
from openai import OpenAI
client_ai = OpenAI()
response = client_ai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful DevOps assistant. Answer in Korean. Be concise."},
{"role": "user", "content": question}
],
max_tokens=1000,
)
answer = response.choices[0].message.content
say(
blocks=[
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Q: {question}*"}
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": answer}
},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": "💡 AI 생성 답변 — 정확성을 확인하세요"}
]
}
]
)
# ===== 앱 홈 탭 =====
@app.event("app_home_opened")
def update_home_tab(client, event):
client.views_publish(
user_id=event["user"],
view={
"type": "home",
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": "🤖 DevOps Bot Dashboard"}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*사용 가능한 명령어:*\n"
"• `/deploy` — 서비스 배포\n"
"• `/status` — 서비스 상태 확인\n"
"• `/ask [질문]` — AI에게 질문\n"
"• `/incident` — 장애 보고"
}
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "*최근 배포:*\n✅ API Server v1.2.3 → Production (2시간 전)\n✅ Web v2.0.1 → Production (어제)"}
},
],
},
)
# ===== 스케줄 메시지 =====
def send_daily_standup_reminder():
"""매일 아침 스탠드업 리마인더"""
import time
app.client.chat_postMessage(
channel="#engineering",
text="🌅 *스탠드업 리마인더*\n오늘의 계획을 스레드에 공유해주세요!\n1. 어제 한 일\n2. 오늘 할 일\n3. 블로커",
)
# ===== 에러 핸들링 =====
@app.error
def handle_errors(error, body, logger):
logger.exception(f"Error: {error}")
logger.info(f"Request body: {body}")
# ===== 실행 =====
if __name__ == "__main__":
# Socket Mode (웹훅 서버 불필요, 방화벽 뒤에서도 동작)
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
장애 관리 봇
@app.command("/incident")
def handle_incident(ack, body, client):
ack()
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "incident_modal",
"title": {"type": "plain_text", "text": "🚨 장애 보고"},
"submit": {"type": "plain_text", "text": "보고"},
"blocks": [
{
"type": "input",
"block_id": "severity",
"element": {
"type": "static_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"},
],
"action_id": "severity_select",
},
"label": {"type": "plain_text", "text": "심각도"},
},
{
"type": "input",
"block_id": "description",
"element": {
"type": "plain_text_input",
"multiline": True,
"placeholder": {"type": "plain_text", "text": "장애 상황을 설명해주세요"},
"action_id": "desc_input",
},
"label": {"type": "plain_text", "text": "설명"},
},
],
},
)
@app.view("incident_modal")
def handle_incident_submit(ack, body, client, view):
ack()
values = view["state"]["values"]
severity = values["severity"]["severity_select"]["selected_option"]["value"]
description = values["description"]["desc_input"]["value"]
reporter = body["user"]["id"]
# 장애 채널 생성
import time
channel_name = f"incident-{int(time.time())}"
channel = client.conversations_create(
name=channel_name,
is_private=False,
)
channel_id = channel["channel"]["id"]
# 장애 초기 메시지
client.chat_postMessage(
channel=channel_id,
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": f"🚨 장애 보고 ({severity})"}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*보고자:* <@{reporter}>\n"
f"*심각도:* {severity}\n"
f"*설명:*\n{description}"
}
},
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "⏱ *타임라인을 이 스레드에 기록해주세요*"
}
},
],
)
# 알림 채널에 공지
client.chat_postMessage(
channel="#incidents",
text=f"🚨 새 장애 발생: {severity}\n📝 {description[:100]}...\n🔗 <#{channel_id}>",
)
Docker 배포
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# docker-compose.yml
version: '3.8'
services:
slack-bot:
build: .
env_file: .env
restart: unless-stopped
healthcheck:
test: ['CMD', 'python', '-c', "import requests; requests.get('http://localhost:3000/health')"]
interval: 30s
timeout: 10s
retries: 3
퀴즈
Q1. Slack Bolt에서 슬래시 명령어 핸들러의 첫 번째 해야 할 일은?
ack() 호출입니다. Slack은 3초 내에 응답(acknowledge)을 받지 못하면 타임아웃 에러를 표시합니다.
Q2. Socket Mode의 장점은?
WebSocket 연결을 사용하므로 공개 URL이나 웹훅 서버가 필요 없습니다. 방화벽 뒤에서도, 로컬 개발에서도 바로 동작합니다.
Q3. Block Kit의 주요 블록 타입 3가지는?
section (텍스트/필드), actions (버튼/셀렉트 등 인터랙티브 요소), input (모달에서 사용자 입력)입니다.
Q4. @app.action과 @app.view의 차이는?
@app.action은 버튼 클릭 등의 인터랙션을, @app.view는 모달 제출(submit)을 처리합니다. 각각 action_id와 callback_id로 매칭됩니다.
Q5. Bot Token(xoxb-)과 App Token(xapp-)의 차이는?
Bot Token은 채널 메시지 전송 등 Slack API 호출에 사용하고, App Token은 Socket Mode WebSocket 연결에 사용합니다.
Q6. 장애 발생 시 자동으로 Slack 채널을 만드는 이유는?
장애 관련 소통을 한 곳에 집중시키고, 타임라인을 기록하며, 사후 분석(포스트모템)을 위한 이력을 남기기 위해서입니다.
Q7. Slack Bot에 LLM을 통합할 때 주의할 점은?
LLM 응답 시간이 3초를 넘을 수 있으므로 먼저 ack()로 응답한 후, 비동기로 LLM을 호출하고 결과를 별도 메시지로 전송해야 합니다.
마무리
Slack Bot은 팀의 생산성을 크게 높이는 도구입니다. Bolt SDK를 사용하면 슬래시 명령어, 모달, 버튼 인터랙션 등을 간결한 Python 코드로 구현할 수 있고, Socket Mode로 별도 서버 없이도 바로 배포할 수 있습니다.