디스코드 봇 만들기#12명령어 쿨다운(Cooldown)·권한(Permission) 관리하기! (파이썬 예제)
목차12개 항목
안녕하세요! Mojangcam입니다 😎
지난 #11편에서는 플라스크(Flask)로 봇 상태를 보는 웹 대시보드를 만들어봤어요. 이제 봇 기능은 꽤 갖춰졌는데, 실제로 서버에 넣어보면 꼭 이런 일이 생깁니다.
누군가 /뽑기 를 초당 10번씩 연타해서 채널이 도배되거나, 아무나 /청소 를 눌러 남의 대화를 통째로 날려버리거나요. 😱
그래서 이번 편에서는 명령어 쿨다운(Cooldown) 과 권한(Permission)·역할(Role) 체크를 배워볼게요. 이 두 가지만 걸어두면 봇이 갑자기 "제대로 운영되는 봇" 느낌이 납니다!

🧠 왜 쿨다운과 권한이 필요할까요?
- 스팸 방지 : 연타로 API 요청이 몰리면 디스코드 레이트 리밋에 걸려 봇이 통째로 먹통이 돼요.
- 밸런스 : 출석·뽑기·경험치처럼 보상이 있는 명령어는 쿨타임이 없으면 의미가 없죠.
- 사고 방지 :
/청소,/밴같은 명령어를 아무나 쓰면 서버가 하루 만에 무너집니다. - 에러 대응 : 체크에 걸렸을 때 봇이 아무 말 없이 조용하면 유저는 "봇 고장났나?" 하고 다시 연타해요. 안내 메시지가 꼭 필요합니다.
📌 준비물
- Python 3.8 이상 (3.10+ 권장)
- discord.py 2.x — 이 글은 최신 버전인 2.7.1 기준이에요
- 디스코드 봇 토큰 (아직 없다면 #1편 Slash Command 완전 정복부터 보고 오세요!)
- 테스트용 디스코드 서버 (역할을 자유롭게 만들 수 있는 곳)
pip install -U discord.py버전 확인은 이렇게 해요.
python -c "import discord; print(discord.__version__)"
# 2.7.1⚠️ 1.x 버전은 슬래시 명령어를 지원하지 않아요. 앞에
2.가 안 붙어 있다면 꼭-U옵션으로 업그레이드해주세요.
📌 1단계 - 쿨다운(Cooldown) 걸기
슬래시 명령어에 쿨타임을 거는 건 데코레이터 한 줄이면 끝나요.
import discord
from discord import app_commands
from discord.ext import commands
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.tree.command(name="출석", description="하루 한 번 출석 체크!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
await interaction.response.send_message(
f"{interaction.user.mention}님, 출석 완료했어요! ✅"
)하나씩 뜯어볼게요 💡
app_commands.checks.cooldown(rate, per, *, key): 슬래시 명령어 전용 쿨다운 데코레이터예요.- rate :
per초 안에 몇 번까지 쓸 수 있는지. 위 코드는1이니까 1번이죠. - per : 쿨타임 길이(초).
60.0이니까 60초에 1번이에요. - key : "누구 기준으로 셀지"를 정하는 함수예요. 반환값은 해시 가능한 값이면 뭐든 OK.
key=lambda i: (i.guild_id, i.user.id): 서버 + 유저 조합이라, 같은 사람이라도 서버가 다르면 쿨타임이 따로 돕니다.
💡
key를 아예 생략하면 기본값은 유저 ID 기준(전 서버 공용) 이에요. 반대로key=None을 주면 모든 사람이 공유하는 글로벌 쿨다운이 됩니다. 서버 전체 이벤트 명령어에 딱이죠!
그리고 데코레이터 순서가 중요해요. @bot.tree.command() 가 항상 맨 위에 와야 합니다. 아래에 붙이면 명령어로 등록이 안 돼요.
📌 2단계 - 쿨타임에 걸렸을 때 안내하기
1단계 코드만 돌리면 쿨타임에 걸렸을 때 봇이 아무 반응도 안 해요. 유저 입장에선 그냥 고장난 봇이죠. 에러 핸들러를 붙여줍시다.
@attend.error
async def attend_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
await interaction.response.send_message(
f"⏳ 너무 빨라요! **{error.retry_after:.1f}초** 뒤에 다시 시도해주세요.",
ephemeral=True,
)하나씩 뜯어볼게요 💡
@명령어이름.error: 그 명령어 전용 에러 핸들러를 등록해요. 파라미터는 반드시(interaction, error)2개여야 합니다.app_commands.CommandOnCooldown: 쿨타임에 걸렸을 때 날아오는 예외예요.error.retry_after: 몇 초 더 기다려야 하는지 알려주는 float 값.:.1f로 소수점 한 자리만 예쁘게 잘랐어요.ephemeral=True: 명령어를 친 본인에게만 보이는 메시지예요. 채널이 안내 메시지로 도배되는 걸 막아줍니다.
💡 참고로
error.cooldown으로 원래 설정값(rate,per)도 꺼내 쓸 수 있어요. "60초에 1번만 가능한 명령어예요" 같은 안내를 넣고 싶을 때 유용합니다.
📌 3단계 - 권한(Permission) 체크하기
이번엔 위험한 명령어를 막아볼게요. 메시지 삭제 명령어는 메시지 관리 권한이 있는 사람만 쓸 수 있어야겠죠.
@bot.tree.command(name="청소", description="채널 메시지를 한 번에 삭제해요")
@app_commands.describe(amount="삭제할 메시지 개수 (1~100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
await interaction.response.defer(ephemeral=True)
deleted = await interaction.channel.purge(limit=amount)
await interaction.followup.send(f"🧹 메시지 {len(deleted)}개를 지웠어요!", ephemeral=True)하나씩 뜯어볼게요 💡
has_permissions(manage_messages=True): 유저가 해당 권한을 가졌는지 확인해요. 없으면MissingPermissions예외가 발생합니다.bot_has_permissions(manage_messages=True): 봇이 그 권한을 가졌는지 확인해요. 없으면BotMissingPermissions. 이걸 빼먹으면 삭제하다 중간에 터집니다!default_permissions(manage_messages=True): 권한 없는 사람에게는 명령어 목록에서 아예 안 보이게 숨겨줍니다.guild_only(): DM에서는 사용할 수 없게 막아요.app_commands.Range[int, 1, 100]: 입력값을 1~100으로 제한해요. 디스코드가 알아서 걸러줍니다.
⚠️
default_permissions만 믿으면 안 돼요! 공식 문서에도 이건 "힌트일 뿐(serves as a hint)"이라고 명시돼 있어요. 서버 관리자가 디스코드 설정에서 이 값을 바꿔버릴 수 있습니다. 그래서 진짜로 막으려면 반드시has_permissions를 같이 걸어야 해요. 또default_permissions는 서브 커맨드에서는 무시되고, 서버 쪽 처리라 에러 핸들러도 호출되지 않습니다.
📌 4단계 - 역할(Role)로 제한하기
권한 대신 "운영진 역할"처럼 서버에서 직접 만든 역할로 막고 싶을 때도 많죠.
@bot.tree.command(name="공지", description="서버 공지를 보내요")
@app_commands.describe(message="공지 내용")
@app_commands.checks.has_any_role("관리자", "운영진")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
embed = discord.Embed(title="📢 공지사항", description=message, color=0x5865F2)
embed.set_footer(text=f"작성자: {interaction.user.display_name}")
await interaction.response.send_message(embed=embed)하나씩 뜯어볼게요 💡
has_role("이름"): 특정 역할 하나를 요구해요. 없으면MissingRole.has_any_role("A", "B"): 나열한 역할 중 하나라도 있으면 통과. 없으면MissingAnyRole.- 역할은 이름(문자열) 대신 ID(정수) 로도 넣을 수 있어요. 이름은 오타·변경에 취약하니 실서비스라면 ID를 권장합니다.
@app_commands.checks.has_any_role(123456789012345678, 987654321098765432)💡 보너스 - VIP는 쿨타임 면제하기 (dynamic_cooldown)
"후원자 역할은 쿨타임을 짧게" 같은 것도 가능해요. dynamic_cooldown 을 쓰면 됩니다.
from typing import Optional
VIP_ROLE_ID = 123456789012345678 # 쿨타임 면제 역할 ID
def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
user = interaction.user
if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
return None # None을 반환하면 쿨타임 없음!
return app_commands.Cooldown(1, 30.0) # 일반 유저는 30초에 1번
@bot.tree.command(name="뽑기", description="랜덤 뽑기를 돌려요")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
await interaction.response.send_message(f"🎲 결과: **{random.randint(1, 100)}점**!")핵심은 None을 반환하면 그 유저는 쿨타임이 통째로 면제된다는 점이에요. 조건에 따라 Cooldown(rate, per) 객체를 직접 만들어 돌려주면 됩니다.
📌 전체 코드 (복붙 OK)
# discord.py 2.7.1 / Python 3.8+
import random
import traceback
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands
TOKEN = "여기에_봇_토큰을_넣으세요"
VIP_ROLE_ID = 123456789012345678 # 쿨타임 면제 역할 ID
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"✅ 로그인 완료: {bot.user}")
# ── 1) 쿨다운: 서버별·유저별 60초에 1번 ──────────────────────
@bot.tree.command(name="출석", description="하루 한 번 출석 체크!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
await interaction.response.send_message(
f"{interaction.user.mention}님, 출석 완료했어요! ✅"
)
# ── 2) 권한 체크: 메시지 관리 권한 보유자만 ──────────────────
@bot.tree.command(name="청소", description="채널 메시지를 한 번에 삭제해요")
@app_commands.describe(amount="삭제할 메시지 개수 (1~100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
await interaction.response.defer(ephemeral=True)
deleted = await interaction.channel.purge(limit=amount)
await interaction.followup.send(f"🧹 메시지 {len(deleted)}개를 지웠어요!", ephemeral=True)
# ── 3) 역할 체크: '관리자' 또는 '운영진'만 ───────────────────
@bot.tree.command(name="공지", description="서버 공지를 보내요")
@app_commands.describe(message="공지 내용")
@app_commands.checks.has_any_role("관리자", "운영진")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
embed = discord.Embed(title="📢 공지사항", description=message, color=0x5865F2)
embed.set_footer(text=f"작성자: {interaction.user.display_name}")
await interaction.response.send_message(embed=embed)
# ── 4) 보너스: VIP는 쿨타임 면제 ────────────────────────────
def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
user = interaction.user
if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
return None
return app_commands.Cooldown(1, 30.0)
@bot.tree.command(name="뽑기", description="랜덤 뽑기를 돌려요")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
await interaction.response.send_message(f"🎲 결과: **{random.randint(1, 100)}점**!")
# ── 5) 전역 에러 핸들러 (모든 명령어 공통) ───────────────────
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
msg = f"⏳ 너무 빨라요! **{error.retry_after:.1f}초** 뒤에 다시 시도해주세요."
elif isinstance(error, app_commands.MissingPermissions):
msg = "🚫 이 명령어를 사용할 권한이 없어요."
elif isinstance(error, app_commands.BotMissingPermissions):
msg = "🤖 봇에게 권한이 없어요! 서버 설정 → 역할에서 봇 권한을 확인해주세요."
elif isinstance(error, app_commands.MissingAnyRole):
msg = "🎭 이 명령어는 관리자·운영진 역할이 필요해요."
elif isinstance(error, app_commands.CheckFailure):
msg = "❌ 실행 조건을 만족하지 않아요."
else:
msg = "⚠️ 알 수 없는 오류가 발생했어요. 잠시 후 다시 시도해주세요."
traceback.print_exception(type(error), error, error.__traceback__)
if interaction.response.is_done():
await interaction.followup.send(msg, ephemeral=True)
else:
await interaction.response.send_message(msg, ephemeral=True)
bot.run(TOKEN)💡 전체 코드에서는 2단계의
@attend.error를 일부러 뺐어요. 이유는 바로 아래 "자주 나는 에러" 3번에서 설명할게요!
📌 실행 결과
봇을 켜고 서버에서 하나씩 눌러보면 이렇게 동작해요.
먼저 /출석 을 치면 출석 완료 메시지가 나와요. 그런데 곧바로 한 번 더 치면? 쿨타임 안내가 나에게만 보입니다.

안내 메시지 아래에 "이 메시지는 본인만 볼 수 있어요"가 붙어 있죠? 이게 바로 ephemeral=True 의 효과예요. 채널에는 아무 흔적도 남지 않습니다.
권한 차이는 명령어 목록에서 바로 드러나요. 같은 서버, 같은 봇인데 계정 권한에 따라 이렇게 달라집니다.

메시지 관리 권한이 없는 계정에는 /청소 가 아예 뜨지 않습니다. default_permissions 가 디스코드 서버 쪽에서 걸러주기 때문이에요. 그리고 혹시 서버 관리자가 권한 설정을 열어줘서 명령어가 보이게 되더라도, has_permissions 가 한 번 더 막아주기 때문에 "🚫 권한이 없어요" 가 뜹니다.
같은 방식으로 /공지 는 관리자·운영진 역할이 없으면 "🎭 역할이 필요해요" 로 거절되고, VIP 역할을 단 계정으로 /뽑기 를 연타하면 쿨타임 없이 계속 돌아갑니다.
📌 자주 나는 에러
1. TypeError: error handler must have 2 parameters
- 원인 : 에러 핸들러 함수의 파라미터 개수가 2개가 아니에요.
self를 붙였거나 하나만 받았을 때 발생해요. - 해결 : 반드시
async def handler(interaction, error):딱 2개로 맞춰주세요. Cog 안에서 쓴다면self를 포함해 3개가 되므로, Cog에서는cog_app_command_error를 오버라이드하세요.
2. 쿨타임에 걸렸는데 봇이 아무 말도 안 해요
- 원인 : 에러 핸들러를 안 붙였어요.
CommandOnCooldown이 발생해도 잡아주는 곳이 없으면 로그에만 남고 유저에겐 아무것도 안 갑니다. - 해결 :
@bot.tree.error전역 핸들러를 꼭 등록해주세요. (전체 코드 5번 블록)
3. 안내 메시지가 두 번 오거나 InteractionResponded 에러가 나요
- 원인 : 개별 핸들러(
@명령어.error)와 전역 핸들러(@bot.tree.error)를 둘 다 등록했기 때문이에요. discord.py는 개별 핸들러를 실행한 뒤에도 전역 핸들러를 항상 호출합니다. 그래서 이미 응답한 인터랙션에 또 응답하려다InteractionResponded가 터져요. - 해결 : 둘 중 하나만 쓰세요. 전체 코드처럼 전역 핸들러로 통일하는 걸 추천해요. 굳이 둘 다 써야 한다면 전역 핸들러 안에서
if interaction.response.is_done(): return으로 빠져나가면 됩니다.
4. MissingPermissions 가 관리자에게도 떠요
- 원인 :
has_permissions는 채널 단위로 계산된 실제 권한을 봅니다. 채널 권한 덮어쓰기(Override)에서 해당 권한이 거부돼 있으면 서버 관리자여도 막힐 수 있어요. - 해결 : 채널 설정 → 권한에서 해당 역할의 권한이 ❌ 로 꺼져 있지 않은지 확인하세요.
5. /청소 가 실행 도중에 멈춰요
- 원인 : 봇에게 메시지 관리 권한이 없거나, 14일이 지난 메시지를 지우려 했을 때예요. 디스코드는 14일 이전 메시지의 일괄 삭제를 금지합니다.
- 해결 :
bot_has_permissions로 먼저 걸러주고, 오래된 채널에서는 개수를 줄여서 테스트하세요.
💬 마무리
오늘은 디스코드 봇에 쿨다운과 권한·역할 체크를 붙여봤어요. 정리하면 이렇습니다 🙌
checks.cooldown(rate, per, key=...)→ 스팸 방지,key로 기준을 자유롭게checks.has_permissions/bot_has_permissions→ 유저·봇 권한 확인checks.has_any_role→ 서버 역할로 제한 (ID 사용 권장)default_permissions→ 목록에서 숨기기, 단 힌트일 뿐이라 단독 사용 금지@bot.tree.error→ 안내 메시지는 한 곳에서 통합 관리
다음 편에서는 디스코드 봇 데이터 저장 - JSON vs SQLite를 다뤄볼게요. 출석 기록이나 포인트를 봇이 꺼져도 남기려면 꼭 필요한 내용이에요. 기대해주세요! 🙌
궁금한 점이나 막히는 부분은 댓글로 남겨주세요 💬
📚 디스코드 봇 만들기 시리즈
- #1 - Slash Command 완전 정복
- #2 - Slash Command에 옵션(Arguments) 추가하기
- #3 - 서브 커맨드(Subcommand) 넣기
- #4 - Slash Command로 임베드 전송하기
- #5 - 버튼(Button) 상호작용 구현하기
- #6 - 드롭다운(Select Menu) 메뉴 구현하기
- #7 - 모달(Modal)로 입력 받기
- #8 - 임베드 이미지·필드 꾸미기
- #9 - AI(Claude·ChatGPT API) 연동하기
- #10 - 다국어(i18n) 지원하기
- #11 - 플라스크(Flask)로 웹 대시보드 만들기
- #12 - 명령어 쿨다운·권한 관리하기 ← 지금 이 글!
👉 함께 보면 좋은 글 : 디스코드 봇 오류 해결 총정리 - intents·token 에러부터 봇이 반응 없을 때까지!
연재 · 32편 중 12번째
디스코드 봇 만들기
- #1디스코드 봇 만들기 - Slash Command 완전 정복!
- #2Slash Command에 옵션(Arguments) 추가하기!
- #3Slash Command에 서브 커맨드(Subcommand) 넣기!
- #4Slash Command로 임베드 전송하기!
- #5버튼(Button) 상호작용 구현하기!
- #6드롭다운(Select Menu) 메뉴 구현하기!
- #7모달(Modal)로 입력 받기 구현하기 (파이썬 예제)
- #8임베드(Embed) 이미지·필드로 꾸미기 (파이썬 예제)
- #9AI(Claude·ChatGPT API) 연동하기! (파이썬 예제)
- #10다국어(i18n) 지원하기! (파이썬 예제)
- #11플라스크(Flask)로 웹 대시보드 만들기! (파이썬 예제)
- #12명령어 쿨다운(Cooldown)·권한(Permission) 관리하기! (파이썬 예제)지금 읽는 글
- #13역할·채널 자동화로 서버 관리하기! (파이썬 예제)
- #14데이터 저장하기! JSON vs SQLite (파이썬 예제)
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제)
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제)
- #30투표·설문(Poll) 봇 만들기! 네이티브 투표 + 버튼으로 실시간 집계까지 (파이썬 예제) 🗳️
- #32서버 로그·감사(Audit Log) 채널 만들기! 메시지 삭제·수정·입퇴장·추방까지 자동 기록 (파이썬 예제) 🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉
