Skip to content

필사 모드: Slack Bot Building Practical Guide — Python Bolt SDK, Event Handling, Slash Commands

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

Introduction

Slack은 전 세계 수십만 팀이 사용하는 업무 커뮤니케이션 플랫폼입니다. 단순한 메시징을 넘어, Slack Bot을 통해 장애 알림, 배포 자동화, 데이터 조회, 승인 워크플로우 등 다양한 업무를 자동화할 수 있습니다.

Slack 공식 프레임워크인 **Bolt for Python**은 이벤트 핸들링, 슬래시 커맨드, 모달, Block Kit 등 Slack의 모든 인터랙션을 간결한 데코레이터 패턴으로 구현할 수 있게 해줍니다. 이 글에서는 앱 설정부터 프로덕션 배포, 에러 핸들링, 실패 복구까지 실전에서 필요한 모든 내용을 다룹니다.

1. Slack 앱 아키텍처와 설정

1.1 Slack 앱 아키텍처 개요

Slack Bot의 동작 흐름은 다음과 같습니다.

1. 사용자가 Slack에서 메시지, 슬래시 커맨드, 버튼 클릭 등의 인터랙션을 수행

2. Slack 서버가 이벤트를 Bot 서버로 전달 (HTTP 또는 WebSocket)

3. Bot이 이벤트를 처리하고 Slack Web API를 통해 응답

4. 사용자에게 메시지, 모달, 리치 블록 등으로 결과 표시

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

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

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

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 --interval=30s --timeout=10s --retries=3 \

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

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 복구 절차 체크리스트

장애 발생 시 다음 순서로 복구를 진행합니다.

1. **로그 확인**: `docker logs slack-bot` 또는 `kubectl logs` 명령으로 에러 메시지 확인

2. **토큰 유효성 검증**: `curl -H "Authorization: Bearer xoxb-..." https://slack.com/api/auth.test` 호출

3. **이벤트 구독 상태 확인**: Slack 앱 설정 페이지에서 Event Subscriptions 상태 점검

4. **네트워크 연결 확인**: Socket Mode의 경우 WebSocket 연결 상태, HTTP Mode의 경우 엔드포인트 접근 가능 여부

5. **프로세스 재시작**: `docker restart slack-bot` 또는 `kubectl rollout restart deployment/slack-bot`

6. **스코프 재확인**: 에러 메시지에 `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인지 확인

Conclusion

Slack Bot은 팀의 업무 효율을 획기적으로 높이는 도구입니다. Python Bolt SDK를 사용하면 이벤트 핸들링, 슬래시 커맨드, 모달, Block Kit 등 Slack의 모든 인터랙션을 간결한 코드로 구현할 수 있습니다.

핵심 정리:

- **ack()는 모든 인터랙션 핸들러의 시작점**입니다. 3초 타임아웃을 항상 기억하세요.

- **Socket Mode**는 개발과 내부 도구에 최적이고, **HTTP Mode**는 프로덕션 규모 서비스에 적합합니다.

- **Block Kit**으로 리치 메시지를 구성하고, **모달**로 사용자 입력을 구조적으로 받으세요.

- 레이트 리밋 핸들링과 에러 복구 절차를 반드시 구현하세요.

- Docker와 Kubernetes로 안정적인 프로덕션 배포 환경을 구축하세요.

References

- [Bolt for Python 공식 문서](https://docs.slack.dev/tools/bolt-python/)

- [Bolt for Python 시작 가이드](https://docs.slack.dev/tools/bolt-python/getting-started/)

- [Slack Block Kit Builder](https://app.slack.com/block-kit-builder)

- [Slack API Rate Limits](https://docs.slack.dev/apis/web-api/rate-limits/)

- [Socket Mode 가이드](https://docs.slack.dev/tools/bolt-python/concepts/socket-mode/)

- [Slack API 공식 문서](https://api.slack.com/docs)

- [slack-bolt PyPI 패키지](https://pypi.org/project/slack-bolt/)

- [bolt-python GitHub 리포지토리](https://github.com/slackapi/bolt-python)

현재 단락 (1/821)

Slack은 전 세계 수십만 팀이 사용하는 업무 커뮤니케이션 플랫폼입니다. 단순한 메시징을 넘어, Slack Bot을 통해 장애 알림, 배포 자동화, 데이터 조회, 승인 워크플로우...

작성 글자: 0원문 글자: 22,934작성 단락: 0/821