디스코드 봇 만들기#16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제)
목차11개 항목
안녕하세요! Mojangcam입니다 😎
지난 #15편에서는 슬래시 명령어를 폴더처럼 묶는 그룹(Group)을 배웠어요. 그런데 지금까지 우리 봇은 전부 "누가 명령어를 쳐야" 움직였죠? 이번 #16편에서는 드디어 봇이 먼저 말을 겁니다.
/리마인더 30 회의 참석! 이렇게 예약해두면 30분 뒤에 봇이 콕 찍어 멘션해주고, 매일 아침 9시에는 알아서 공지를 올려주는 기능이에요. 스터디 알림, 서버 점검 공지, 출석 체크 안내처럼 실제로 제일 많이 쓰이는 기능이니 꼭 따라 만들어보세요!

🧠 왜 time.sleep()을 쓰면 안 될까요?
"30분 뒤에 보내려면 time.sleep(1800) 쓰면 되는 거 아닌가요?" 하실 수 있는데, 절대 안 돼요. time.sleep()은 파이썬 전체를 멈춰 세우는 함수라서, 봇이 30분 동안 통째로 얼어붙어요. 그동안 다른 사람이 명령어를 쳐도 아무 반응이 없고, 심하면 디스코드 연결까지 끊깁니다.
봇처럼 여러 일을 동시에 처리해야 하는 프로그램에서는 비동기(async) 방식으로 기다려야 해요. 그리고 그 귀찮은 작업을 discord.py가 통째로 대신해주는 도구가 바로 discord.ext.tasks랍니다. 인터넷이 잠깐 끊겼을 때 자동 재연결까지 알아서 해줘요!
🧩 tasks.loop, 딱 두 가지 방식만 기억하세요
| 방식 | 사용법 | 이럴 때 써요 |
|---|---|---|
| 주기 반복 | @tasks.loop(seconds=20) |
"20초마다 확인해" — 예약 알림 감시용 |
| 정각 실행 | @tasks.loop(time=시각) |
"매일 아침 9시에 실행해" — 정기 공지용 |
두 방식은 같이 못 씁니다. seconds와 time을 동시에 넣으면 TypeError가 나요. 오늘은 둘 다 하나씩 만들어볼 거예요. 참고로 tasks는 discord.py 1.1부터 있었지만, time= 옵션은 2.0부터 추가됐어요.
📌 준비물
- Python 3.8 이상
- discord.py 2.x (최신 2.x 버전을 설치하면 돼요)
- 봇 토큰과 서버에 초대된 봇 (시리즈 앞편에서 만든 그대로!)
- 공지를 보낼 채널 ID (디스코드 설정에서 개발자 모드를 켜고, 채널 우클릭 → "채널 ID 복사")
pip install -U discord.py이번 편도 슬래시 명령어만 쓰기 때문에 Message Content Intent는 켜지 않아도 돼요.
📌 1단계 - 20초마다 도는 백그라운드 루프 만들기
가장 기본이 되는 반복 루프부터 만들어볼게요.
import discord
from discord.ext import commands, tasks
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
@tasks.loop(seconds=20)
async def reminder_checker():
print("⏱️ 알림 확인 중...")
@reminder_checker.before_loop
async def before_reminder_checker():
await bot.wait_until_ready() # 봇이 완전히 켜질 때까지 기다려요
@bot.event
async def on_ready():
if not reminder_checker.is_running():
reminder_checker.start() # 루프 시작!
print(f"✅ {bot.user} 로그인 완료!")하나씩 뜯어볼게요 💡
- @tasks.loop(seconds=20) : 이 함수를 20초마다 반복 실행해요.
minutes=,hours=도 똑같이 쓸 수 있어요. - @reminder_checker.before_loop : 루프가 처음 돌기 직전에 한 번 실행되는 함수예요. 여기서
await bot.wait_until_ready()로 봇 준비를 기다리는 게 국룰이에요. 이걸 빼면 아직 채널 정보가 없는 상태에서 루프가 돌아 에러가 납니다! - reminder_checker.start() : 루프는 만들어두기만 하면 안 돌아요. 반드시
start()로 시동을 걸어야 해요. - is_running() 체크 :
on_ready는 봇이 재연결될 때 여러 번 호출될 수 있어요. 가드 없이start()를 또 부르면RuntimeError가 나기 때문에 꼭 감싸주세요.
📌 2단계 - /리마인더 명령어로 알림 예약하기
이제 유저가 예약을 걸 수 있게 만들어요. 예약 정보를 리스트에 담아두고, 1단계에서 만든 루프가 20초마다 훑으면서 시간이 된 것만 발사하는 구조예요.
import datetime
from discord import app_commands
reminders = [] # 예약된 알림을 담아둘 목록
@bot.tree.command(name="리마인더", description="정해진 시간 뒤에 알림을 보내드려요")
@app_commands.describe(분="몇 분 뒤에 알릴까요? (1~1440)", 내용="알림에 적을 내용")
async def add_reminder(
interaction: discord.Interaction,
분: app_commands.Range[int, 1, 1440],
내용: str,
):
run_at = discord.utils.utcnow() + datetime.timedelta(minutes=분)
reminders.append({
"user_id": interaction.user.id,
"channel_id": interaction.channel_id,
"content": 내용,
"run_at": run_at,
})
await interaction.response.send_message(
f"✅ {discord.utils.format_dt(run_at, style='R')}에 알려드릴게요!\n> {내용}",
ephemeral=True,
)하나씩 뜯어볼게요 💡
- discord.utils.utcnow() : 시간대(UTC) 정보가 붙어 있는 "지금"이에요. 그냥
datetime.now()를 쓰면 시간대 정보가 없어서 나중에 비교할 때TypeError가 나니 이걸 쓰세요! - app_commands.Range[int, 1, 1440] : 입력값을 1~1440 사이 정수로 제한해요. 유저가
-5나99999를 넣는 사고를 디스코드가 알아서 막아줍니다. (1440분 = 24시간) - discord.utils.format_dt(run_at, style='R') : 디스코드 전용 시간 태그를 만들어줘요.
style='R'을 주면 화면에 "30분 후" 같은 상대 시간으로 표시되고, 보는 사람의 시간대에 맞춰 자동 변환까지 돼요. 정말 편해요! - ephemeral=True : 예약 확인 메시지는 본인에게만 보여줘서 채팅창을 깨끗하게 유지해요.
그리고 1단계의 루프 안을 이렇게 채워줍니다.
@tasks.loop(seconds=20)
async def reminder_checker():
now = discord.utils.utcnow()
done = []
for r in reminders:
if r["run_at"] <= now: # 시간이 됐다면!
channel = bot.get_channel(r["channel_id"])
if channel is not None:
embed = discord.Embed(
title="⏰ 리마인더",
description=r["content"],
color=0x5865F2,
)
await channel.send(content=f"<@{r['user_id']}>", embed=embed)
done.append(r)
for r in done:
reminders.remove(r) # 보낸 건 목록에서 제거반복문을 도는 도중에 리스트에서 항목을 지우면 안 돼요. 순서가 밀려서 일부를 건너뛰게 되거든요. 그래서 보낸 항목을 done에 모아뒀다가 루프가 끝난 뒤에 한꺼번에 지우는 거예요. 이건 파이썬 전반에서 통하는 습관이니 기억해두면 좋아요!
📌 3단계 - 매일 아침 9시에 자동 공지하기
이번엔 time= 방식이에요. 시간대(tzinfo)를 꼭 붙여야 한다는 게 오늘의 최대 함정 포인트입니다.
KST = datetime.timezone(datetime.timedelta(hours=9)) # 한국 시간(UTC+9)
@tasks.loop(time=datetime.time(hour=9, minute=0, tzinfo=KST))
async def daily_notice():
channel = bot.get_channel(NOTICE_CHANNEL_ID)
if channel is None:
return
today = datetime.datetime.now(KST).strftime("%Y-%m-%d")
await channel.send(f"☀️ 좋은 아침이에요! 오늘은 {today} 입니다. 오늘도 화이팅! 💪")
@daily_notice.before_loop
async def before_daily_notice():
await bot.wait_until_ready()하나씩 뜯어볼게요 💡
- KST = datetime.timezone(datetime.timedelta(hours=9)) : 한국 표준시를 만드는 한 줄이에요.
tzinfo를 안 주면 UTC로 해석돼서, 9시로 적었는데 실제로는 한국 시간 오후 6시에 알림이 갑니다. 시간이 엉뚱하다면 십중팔구 이것 때문이에요! - time= : 하루 중 정해진 시각에 실행해요. 여러 시각을 원하면 리스트로
time=[time(9,0,tzinfo=KST), time(18,0,tzinfo=KST)]처럼 넘기면 됩니다. - bot.get_channel(...) : 채널 객체를 가져와요. 봇이 못 보는 채널이거나 ID가 틀리면
None이 오니 꼭 확인하고 쓰세요.
📌 전체 코드 (복붙 OK)
TOKEN과 NOTICE_CHANNEL_ID만 바꾸면 바로 돌아가는 완성본이에요. 예약 알림(/리마인더), 내 예약 확인(/리마인더목록), 매일 아침 공지까지 전부 들어 있어요.
# discord.py 2.x / Python 3.8+
import datetime
import discord
from discord import app_commands
from discord.ext import commands, tasks
TOKEN = "여기에_봇_토큰"
NOTICE_CHANNEL_ID = 123456789012345678 # 매일 공지를 보낼 채널 ID
KST = datetime.timezone(datetime.timedelta(hours=9)) # 한국 시간(UTC+9)
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
# 예약된 알림 목록 (봇을 끄면 사라져요! 저장은 #14편 참고)
reminders = []
# ── 20초마다 예약 알림을 확인하는 루프 ──
@tasks.loop(seconds=20)
async def reminder_checker():
now = discord.utils.utcnow()
done = []
for r in reminders:
if r["run_at"] <= now:
channel = bot.get_channel(r["channel_id"])
if channel is not None:
embed = discord.Embed(
title="⏰ 리마인더",
description=r["content"],
color=0x5865F2,
)
await channel.send(content=f"<@{r['user_id']}>", embed=embed)
done.append(r)
for r in done:
reminders.remove(r)
@reminder_checker.before_loop
async def before_reminder_checker():
await bot.wait_until_ready()
@reminder_checker.error
async def reminder_checker_error(error):
print(f"[리마인더 루프 오류] {error!r}")
# ── 매일 아침 9시 정기 공지 루프 ──
@tasks.loop(time=datetime.time(hour=9, minute=0, tzinfo=KST))
async def daily_notice():
channel = bot.get_channel(NOTICE_CHANNEL_ID)
if channel is None:
return
today = datetime.datetime.now(KST).strftime("%Y-%m-%d")
await channel.send(f"☀️ 좋은 아침이에요! 오늘은 {today} 입니다. 오늘도 화이팅! 💪")
@daily_notice.before_loop
async def before_daily_notice():
await bot.wait_until_ready()
# ── 슬래시 명령어 ──
@bot.tree.command(name="리마인더", description="정해진 시간 뒤에 알림을 보내드려요")
@app_commands.describe(분="몇 분 뒤에 알릴까요? (1~1440)", 내용="알림에 적을 내용")
async def add_reminder(
interaction: discord.Interaction,
분: app_commands.Range[int, 1, 1440],
내용: str,
):
run_at = discord.utils.utcnow() + datetime.timedelta(minutes=분)
reminders.append({
"user_id": interaction.user.id,
"channel_id": interaction.channel_id,
"content": 내용,
"run_at": run_at,
})
await interaction.response.send_message(
f"✅ {discord.utils.format_dt(run_at, style='R')}에 알려드릴게요!\n> {내용}",
ephemeral=True,
)
@bot.tree.command(name="리마인더목록", description="내가 예약한 알림을 확인해요")
async def list_reminders(interaction: discord.Interaction):
mine = [r for r in reminders if r["user_id"] == interaction.user.id]
if not mine:
await interaction.response.send_message("예약된 알림이 없어요! 🕊️", ephemeral=True)
return
embed = discord.Embed(title="📋 내가 예약한 알림", color=0x5865F2)
for i, r in enumerate(mine, start=1):
embed.add_field(
name=f"{i}. {r['content']}",
value=discord.utils.format_dt(r["run_at"], style="R"),
inline=False,
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@bot.event
async def on_ready():
if not reminder_checker.is_running():
reminder_checker.start()
if not daily_notice.is_running():
daily_notice.start()
synced = await bot.tree.sync()
print(f"✅ {bot.user} 로그인 완료! 명령어 {len(synced)}개 동기화, 알림 루프 시작!")
bot.run(TOKEN)📌 실행 결과
봇을 켜고 디스코드에서 확인해봐요.
- 터미널에 "✅ MojangBot 로그인 완료! 명령어 2개 동기화, 알림 루프 시작!" 이 뜨면 성공이에요.
/리마인더 분:1 내용:물 마시기실행 → "✅ 1분 후에 알려드릴게요!" 라고 나에게만 보이는 확인 메시지가 떠요./리마인더목록→ 예약해둔 알림이 임베드로 정리되어 나와요. 남은 시간은 "1분 후" 처럼 알아서 카운트다운돼요.- 1분 뒤 → 봇이
@나를 멘션하면서 ⏰ 리마인더 임베드를 띄워줘요. 알림 뜨는 순간이 제일 뿌듯해요! 🎉 - 다음 날 아침 9시 → 지정한 채널에 아침 인사 공지가 자동으로 올라와요.
⏱️ 딱 1분 뒤에 칼같이 오지는 않아요. 루프가 20초마다 확인하니까 최대 20초 정도 늦게 도착할 수 있어요. 예약 확인 메시지의 남은 시간이 "1분 후" → "0초 후" → "1초 전", "2초 전"으로 넘어가도 고장 난 게 아니라, 다음 검사 차례를 기다리는 중이에요. 더 촘촘하게 맞추고 싶다면 @tasks.loop(seconds=5)처럼 주기를 줄이면 됩니다. (대신 그만큼 자주 도니 너무 짧게는 두지 마세요!)


📌 자주 나는 에러
1. 명령어는 되는데 알림이 안 와요
원인: 루프를 start() 하지 않았어요. 데코레이터만 붙이면 루프는 잠자는 상태예요.
해결: on_ready 안에 reminder_checker.start()가 있는지 확인하세요. 터미널에 아무 로그도 안 뜬다면 이 경우가 대부분이에요.
2. RuntimeError: Task is already launched and is running
원인: on_ready가 재연결 때 다시 호출되면서 start()를 두 번 불렀어요.
해결: 전체 코드처럼 if not reminder_checker.is_running(): 으로 감싸주세요.
3. 매일 알림이 엉뚱한 시간에 와요 (9시로 했는데 저녁 6시에 옴)
원인: datetime.time(hour=9)에 tzinfo를 안 줬어요. 시간대를 안 주면 UTC로 해석되고, 한국은 UTC+9라서 정확히 9시간 밀립니다.
해결: datetime.time(hour=9, minute=0, tzinfo=KST) 처럼 tzinfo를 꼭 붙이세요.
4. TypeError: can't compare offset-naive and offset-aware datetimes
원인: 시간대 정보가 있는 시각과 없는 시각을 비교했어요. datetime.datetime.now()에는 시간대 정보가 없거든요.
해결: 봇 안에서 "지금"이 필요할 땐 항상 discord.utils.utcnow() 로 통일하세요. 이 습관 하나로 시간 관련 버그 절반이 사라져요.
5. AttributeError: 'NoneType' object has no attribute 'send'
원인: bot.get_channel()이 None을 돌려줬어요. 채널 ID가 틀렸거나, 봇이 그 채널을 볼 권한이 없거나, 아직 캐시에 없는 경우예요.
해결: 채널 ID를 다시 복사해 확인하고, 봇 권한도 점검하세요. 그래도 안 되면 channel = await bot.fetch_channel(ID) 로 디스코드에 직접 물어보면 됩니다.
6. 루프가 조용히 멈췄어요
원인: 루프 안에서 처리되지 않은 예외가 터지면 루프가 죽어버려요. 그런데 화면에는 아무것도 안 뜰 때가 많아요.
해결: 전체 코드처럼 @루프이름.error 핸들러를 달아 무슨 일이 있었는지 찍어보세요. 살아있는지는 reminder_checker.is_running()으로 확인할 수 있어요.
7. 시간이 됐는데 알림이 몇 초 늦게 와요
원인: 고장이 아니라 정상 동작이에요. 루프가 20초마다 한 번씩만 확인하니까, 예약 시각과 검사 시각 사이에 최대 20초 공백이 생깁니다.
해결: 그대로 두셔도 됩니다. 초 단위로 정확해야 한다면 @tasks.loop(seconds=5)로 주기를 줄이거나, 돌아가는 중에 reminder_checker.change_interval(seconds=5)로 바꾸세요.
💡 보너스 팁
- 간격을 실시간으로 바꾸기 :
reminder_checker.change_interval(seconds=5)한 줄이면 돌아가는 중에도 주기를 바꿀 수 있어요. 평소엔 느슨하게, 이벤트 때만 촘촘하게 돌릴 수 있죠. - 다음 실행 시각 확인 :
daily_notice.next_iteration으로 다음 실행 예정 시각을 볼 수 있어요./상태명령어를 만들어 보여주면 꽤 그럴듯해요! - 봇을 꺼도 예약이 살아남게 : 지금은
reminders가 메모리에만 있어서 재시작하면 전부 사라져요. #14편의 JSON·SQLite 저장을 붙이면 완성도가 확 올라갑니다. - 명령어가 늘어난다면 :
/알림 추가,/알림 목록처럼 #15편의 그룹(Group)으로 묶어주면 훨씬 깔끔해요. - 진짜 24시간 돌리려면 : 내 PC를 계속 켜둘 순 없으니 파이썬 VPS 배포 입문 글을 참고해 서버에 올려보세요. 리마인더 봇은 24시간 떠 있어야 제값을 합니다!
- 봇 없이 알림만 필요하다면 : 디스코드 웹훅만으로도 알림을 보낼 수 있어요. 파이썬 업무 자동화 #4 - 웹훅으로 알림 보내기를 확인해보세요.
📌 마무리
오늘은 봇이 먼저 말을 거는 리마인더·정기 알림 기능을 만들었어요. 핵심은 딱 세 가지예요. time.sleep() 대신 tasks.loop 쓰기, before_loop에서 wait_until_ready() 로 기다리기, 그리고 정각 실행에는 tzinfo 꼭 붙이기! 이 세 가지만 지키면 시간 관련 사고는 거의 안 납니다.
다음 편에서는 유저가 명령어를 입력할 때 후보를 착착 띄워주는 자동완성(Autocomplete) 을 만들어볼게요. 기대해주세요! 🙌
📚 디스코드 봇 만들기 시리즈
- #5 - 버튼(Button) 상호작용
- #6 - 드롭다운(Select Menu) 메뉴
- #7 - 모달(Modal)로 입력 받기
- #8 - 임베드(Embed) 꾸미기
- #9 - AI(Claude·ChatGPT API) 연동
- #10 - 다국어(i18n) 지원
- #11 - 플라스크(Flask) 웹 대시보드
- #12 - 명령어 쿨다운·권한 관리
- #13 - 역할·채널 자동화
- #14 - 데이터 저장 (JSON vs SQLite)
- #15 - 슬래시 명령어 그룹(Group)
- #16 - 리마인더·알림 기능 ← 지금 이 글!
연재 · 32편 중 16번째
디스코드 봇 만들기
- #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임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉
