디스코드 봇 만들기#30투표·설문(Poll) 봇 만들기! 네이티브 투표 + 버튼으로 실시간 집계까지 (파이썬 예제) 🗳️
목차10개 항목
안녕하세요! Mojangcam입니다 😎
지난 편에서는 화폐·상점(Economy) 시스템을 만들어서 코인을 벌고 아이템을 사는 봇을 완성했어요. 서버에 "놀거리"가 하나 생긴 셈이죠!
이번 편에서는 서버 운영에 진짜 자주 쓰이는 기능, 투표·설문(Poll) 봇을 만들어볼게요. 두 가지 방식을 다 다룹니다.
- 네이티브 투표 — 디스코드가 자체 제공하는 투표 UI를 봇이 띄우는 방식 (제일 간단!)
- 버튼 커스텀 투표 — 버튼으로 직접 만들어서 실시간 집계·중복 방지·투표 마감까지 내 맘대로 제어하는 방식
서버 이벤트 날짜 정하기, 다음 콘텐츠 정하기, 규칙 개정 찬반 묻기... 이런 거 매번 이모지 달아서 손으로 세고 계셨다면 오늘 글로 끝낼 수 있어요! 🙌

📌 준비물
- Python 3.8+ (권장 3.10 이상)
- discord.py 2.x — 네이티브 Poll은 2.5 이상이 필요해요
- python-dotenv — 봇 토큰을 코드에 직접 쓰지 않기 위해 필수!
pip install -U "discord.py>=2.5" python-dotenv설치된 버전은 이렇게 확인해요.
python -c "import discord; print(discord.__version__)"그리고 봇 토큰은 절대 코드에 하드코딩하지 마세요. 프로젝트 폴더에 .env 파일을 만들어 주세요.
token=여기에_봇_토큰_붙여넣기💡 팁:
.gitignore에.env한 줄 꼭 추가하세요! 깃허브에 토큰이 올라가면 디스코드가 자동으로 토큰을 무효화시켜 버려요. (그나마 다행이지만요 😅)
🧩 인텐트(Intents) 설정
네이티브 투표의 투표 이벤트를 받으려면 polls 인텐트가 필요해요. 버튼 방식만 쓸 거면 없어도 됩니다.
intents = discord.Intents.default()
intents.message_content = True
intents.polls = True # 투표 이벤트(on_poll_vote_add 등)를 받으려면 필요📌 1단계 - 네이티브 투표 띄우기 (제일 쉬운 길)
discord.py 2.5부터 discord.Poll 클래스가 생겨서, 디스코드 앱의 기본 투표 UI를 봇이 그대로 띄울 수 있어요.
@bot.tree.command(name="투표", description="디스코드 기본 투표를 띄웁니다")
async def native_poll(interaction: discord.Interaction, 질문: str, 항목: str):
# 항목은 쉼표로 구분해서 받아요: "치킨,피자,족발"
choices = [c.strip() for c in 항목.split(",") if c.strip()][:10]
if len(choices) < 2:
await interaction.response.send_message("항목은 2개 이상 넣어주세요!", ephemeral=True)
return
poll = discord.Poll(question=질문, duration=datetime.timedelta(hours=24))
for c in choices:
poll.add_answer(text=c)
await interaction.response.send_message(poll=poll)하나씩 뜯어볼게요 💡
- discord.Poll(question, duration) : 투표 객체를 만들어요.
question은 최대 300자,duration은datetime.timedelta로 지정하며 최대 32일(768시간)까지 가능해요. - poll.add_answer(text=..., emoji=...) : 선택지를 추가해요. 최대 10개까지만 됩니다.
emoji인자로 이모지도 붙일 수 있어요. - send_message(poll=poll) : 메시지에
poll=으로 넘기면 끝! 집계·마감·중복 방지는 디스코드가 알아서 해줍니다. - 참고로 봇에게 투표 만들기(Send Polls) 권한이 있어야 해요.
투표가 들어오고 나가는 순간을 잡고 싶다면 이벤트를 쓰면 돼요.
@bot.event
async def on_poll_vote_add(user, answer):
print(f"{user}님이 '{answer.text}'에 투표했어요")💬 이 이벤트는 해당 메시지가 봇 캐시에 있을 때만 호출돼요. 오래된 메시지까지 잡으려면
on_raw_poll_vote_add를 쓰세요.
네이티브 투표는 정말 편하지만 한계가 있어요. 항목 10개 제한, 역할 제한 불가, 결과를 내 임베드에 예쁘게 못 그림, 투표 도중 집계를 가공할 수 없음. 그래서 2단계로 갑니다!
📌 2단계 - 버튼 커스텀 투표 (실시간 집계)
버튼 하나당 선택지 하나를 만들고, 누가 뭘 눌렀는지 딕셔너리에 저장하는 구조예요.
class PollView(discord.ui.View):
def __init__(self, question: str, choices: list[str], author_id: int, timeout: float = 3600):
super().__init__(timeout=timeout)
self.question = question
self.choices = choices
self.author_id = author_id
self.votes: dict[int, int] = {} # {유저ID: 선택지 index}
for idx, label in enumerate(choices):
self.add_item(VoteButton(label=label, index=idx))하나씩 뜯어볼게요 💡
- self.votes : 핵심이에요. 키가 유저 ID라서 같은 사람이 다시 누르면 값이 덮어씌워져요. 즉 중복 투표가 구조적으로 막힙니다. 마음 바뀌면 다른 버튼 누르면 끝!
- timeout : 이 시간(초)이 지나면 버튼이 자동으로 멈춰요. 3600 = 1시간.
- author_id : 투표를 연 사람만 "조기 마감"을 누를 수 있게 하려고 저장해둡니다.
버튼 본체는 이렇게 생겼어요.
class VoteButton(discord.ui.Button):
def __init__(self, label: str, index: int):
super().__init__(label=label, style=discord.ButtonStyle.secondary)
self.index = index
async def callback(self, interaction: discord.Interaction):
view: PollView = self.view
view.votes[interaction.user.id] = self.index
await interaction.response.edit_message(embed=view.build_embed())- callback : 버튼을 누르는 순간 실행돼요. 표를 기록하고 바로 메시지를 다시 그립니다. 이게 "실시간 집계"의 정체예요!
- edit_message : 새 메시지를 보내지 않고 기존 메시지를 갈아끼워요. 채팅창이 안 더러워집니다 👍
📌 3단계 - 결과를 막대그래프로 그리기
숫자만 보여주면 심심하니까, 블록 문자로 막대그래프를 만들어볼게요.
def build_embed(self) -> discord.Embed:
counts = [0] * len(self.choices)
for choice_idx in self.votes.values():
counts[choice_idx] += 1
total = sum(counts)
lines = []
for label, cnt in zip(self.choices, counts):
ratio = (cnt / total) if total else 0
bar = "█" * round(ratio * 12) or "░"
lines.append(f"`{bar:<12}` **{cnt}표** ({ratio*100:.0f}%) · {label}")
embed = discord.Embed(title=f"🗳️ {self.question}", description="\n".join(lines))
embed.set_footer(text=f"총 {total}표 · 버튼을 눌러 투표하세요 (다시 누르면 변경돼요)")
return embed- bar:<12 : 백틱으로 감싸고 12칸으로 폭을 고정해서 막대 길이가 들쭉날쭉하지 않게 맞춰요.
- or "░" : 0표일 때 빈 줄이 되지 않게 빈 칸 문자를 하나 넣어줍니다.
- 총 투표수는
len(self.votes)와 같아요. 한 사람당 한 표니까요!

📌 전체 코드 (복붙 OK)
# discord.py 2.5+ / Python 3.10+
# pip install -U "discord.py>=2.5" python-dotenv
import os
import datetime
import discord
from discord.ext import commands
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("token")
intents = discord.Intents.default()
intents.message_content = True
intents.polls = True
bot = commands.Bot(command_prefix="!", intents=intents)
class VoteButton(discord.ui.Button):
def __init__(self, label: str, index: int):
super().__init__(label=label, style=discord.ButtonStyle.secondary)
self.index = index
async def callback(self, interaction: discord.Interaction):
view: "PollView" = self.view
if view.closed:
await interaction.response.send_message("이미 마감된 투표예요!", ephemeral=True)
return
view.votes[interaction.user.id] = self.index
await interaction.response.edit_message(embed=view.build_embed(), view=view)
class CloseButton(discord.ui.Button):
def __init__(self):
super().__init__(label="투표 마감", style=discord.ButtonStyle.danger, row=4)
async def callback(self, interaction: discord.Interaction):
view: "PollView" = self.view
if interaction.user.id != view.author_id:
await interaction.response.send_message("투표를 연 사람만 마감할 수 있어요!", ephemeral=True)
return
view.closed = True
for item in view.children:
item.disabled = True
await interaction.response.edit_message(embed=view.build_embed(), view=view)
view.stop()
class PollView(discord.ui.View):
def __init__(self, question: str, choices: list[str], author_id: int, timeout: float = 3600):
super().__init__(timeout=timeout)
self.question = question
self.choices = choices
self.author_id = author_id
self.votes: dict[int, int] = {}
self.closed = False
for idx, label in enumerate(choices):
self.add_item(VoteButton(label=label, index=idx))
self.add_item(CloseButton())
def build_embed(self) -> discord.Embed:
counts = [0] * len(self.choices)
for choice_idx in self.votes.values():
counts[choice_idx] += 1
total = sum(counts)
lines = []
for label, cnt in zip(self.choices, counts):
ratio = (cnt / total) if total else 0
bar = "█" * round(ratio * 12) or "░"
lines.append(f"`{bar:<12}` **{cnt}표** ({ratio * 100:.0f}%) · {label}")
title = ("🔒 [마감] " if self.closed else "🗳️ ") + self.question
embed = discord.Embed(title=title, description="\n".join(lines), color=0x5865F2)
footer = f"총 {total}표"
footer += " · 최종 결과입니다" if self.closed else " · 버튼을 눌러 투표하세요 (다시 누르면 변경돼요)"
embed.set_footer(text=footer)
return embed
async def on_timeout(self):
self.closed = True
for item in self.children:
item.disabled = True
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"{bot.user} 준비 완료!")
@bot.tree.command(name="투표", description="버튼 투표를 시작합니다 (항목은 쉼표로 구분)")
async def poll_command(interaction: discord.Interaction, 질문: str, 항목: str, 시간: int = 1):
choices = [c.strip() for c in 항목.split(",") if c.strip()][:5]
if len(choices) < 2:
await interaction.response.send_message("항목은 2개 이상 넣어주세요! 예) 치킨,피자,족발", ephemeral=True)
return
view = PollView(질문, choices, interaction.user.id, timeout=시간 * 3600)
await interaction.response.send_message(embed=view.build_embed(), view=view)
@bot.tree.command(name="기본투표", description="디스코드 네이티브 투표를 띄웁니다")
async def native_poll(interaction: discord.Interaction, 질문: str, 항목: str, 시간: int = 24):
choices = [c.strip() for c in 항목.split(",") if c.strip()][:10]
if len(choices) < 2:
await interaction.response.send_message("항목은 2개 이상 넣어주세요!", ephemeral=True)
return
poll = discord.Poll(question=질문, duration=datetime.timedelta(hours=시간))
for c in choices:
poll.add_answer(text=c)
await interaction.response.send_message(poll=poll)
@bot.event
async def on_poll_vote_add(user, answer):
print(f"[투표] {user}님 → {answer.text}")
bot.run(token)📌 실행 결과
봇을 켜고 서버에서 이렇게 써보세요.
/투표 질문:오늘 저녁 뭐 먹을까요? 항목:치킨,피자,족발 시간:2버튼 3개와 "투표 마감" 버튼이 달린 임베드가 뜹니다. 누군가 버튼을 누르는 즉시 막대그래프가 갱신돼요. 같은 사람이 다른 버튼을 누르면 표가 이동하고, 같은 버튼을 다시 눌러도 표가 늘어나지 않아요.
투표를 연 사람이 "투표 마감"을 누르면 모든 버튼이 비활성화되고 제목이 🔒 [마감]으로 바뀝니다. 아무도 안 눌러도 시간(기본 1시간)이 지나면 on_timeout이 알아서 잠가줘요.
네이티브 투표는 /기본투표로 확인해보세요. 디스코드 앱의 익숙한 그 투표 UI가 그대로 뜹니다!

📌 자주 나는 에러
1. AttributeError: module 'discord' has no attribute 'Poll'
- 원인: discord.py 버전이 2.5보다 낮아요.
Poll클래스는 2.5에서 추가됐습니다. - 해결:
pip install -U "discord.py>=2.5"후python -c "import discord; print(discord.__version__)"로 확인하세요.
2. AttributeError: 'Intents' object has no attribute 'polls'
- 원인: 위와 같은 버전 문제예요.
polls인텐트도 2.5에서 들어왔어요. - 해결: 업그레이드하거나, 버튼 방식만 쓸 거면
intents.polls = True줄을 지우면 됩니다.
3. 투표 버튼을 눌렀는데 "상호작용이 실패했습니다"가 떠요
- 원인: 봇을 재시작했기 때문이에요.
View객체는 메모리에 살아있는 동안만 동작하거든요. - 해결: 짧은 투표는 그냥 다시 열면 되고, 봇이 꺼져도 유지되는 투표를 원하면
timeout=None+custom_id를 지정한 영구 View(persistent view)로 만들고 표를 DB(SQLite)에 저장해야 해요. 표를 메모리 딕셔너리에만 두면 재시작 시 전부 날아갑니다!
4. Missing Permissions — 네이티브 투표가 안 올라가요
- 원인: 봇 역할에 투표 만들기(Send Polls) 권한이 없어요.
- 해결: 서버 설정 → 역할 → 봇 역할에서 해당 권한을 켜주세요. 채널별 권한 덮어쓰기도 같이 확인하고요.
5. 슬래시 명령어가 서버에 안 보여요
- 원인: 전역 동기화(
bot.tree.sync())는 반영까지 최대 1시간 걸릴 수 있어요. - 해결: 테스트할 땐
await bot.tree.sync(guild=discord.Object(id=서버ID))로 특정 서버에만 동기화하면 즉시 반영됩니다.
📌 마무리
오늘은 투표·설문 봇을 두 가지 방식으로 만들어봤어요 🙌
- 네이티브 투표 — 코드 5줄이면 끝. 집계·마감을 디스코드가 다 해줘서 제일 편해요
- 버튼 커스텀 투표 — 실시간 막대그래프, 중복 방지, 조기 마감까지 내 맘대로 제어 가능
"빠르게 물어보고 싶다" → 네이티브, "결과를 예쁘게 보여주고 조건을 걸고 싶다" → 버튼. 이렇게 기억하시면 돼요!
다음 편에서는 이 투표 결과를 데이터베이스에 저장해서 봇을 재시작해도 안 날아가게 만드는 방법을 다뤄볼게요. 위 3번 에러의 근본 해결편이 되겠네요. 기대해주세요! 😎
📚 디스코드 봇 만들기 시리즈
- 시리즈 총정리 - #1부터 순서대로 보는 로드맵 🗺️
- #28 - 레벨·경험치(XP) 시스템 만들기 → https://mojangcam.tistory.com/113
- #29 - 화폐·상점(Economy) 시스템 만들기 (공개 예정)
- #30 - 투표·설문(Poll) 봇 만들기 ← 지금 이 글!
- #31 - 투표 결과 DB에 저장하기 (예정)
연재 · 32편 중 30번째
디스코드 봇 만들기
- #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임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉
