디스코드 봇 개발자용 2026년 9월 API 변경 총정리 - 파일 20MiB·슬래시 커맨드 여러 줄 입력·연령 확인(Age Assurance)·초대 대상 유저·shard 필수, discord.py 2.7.1로 대응하는 법 (파이썬 예제) 🗞️
한눈에 보기
2026년 8월 말부터 9월까지 디스코드 개발자 체인지로그에 올라온 변경 7가지를 봇 개발자 기준으로 정리하고, 코드를 고쳐야 하는 것과 안 고쳐도 되는 것을 나눠요. discord.py 2.7.1에 아직 없는 기능은 엔드포인트를 직접 호출하는 예제로 대응합니다.
- discord.py 2.7.1 · Python 3.13 · 2026-09-30 공식 체인지로그 기준으로 확인 (예제는 문법·import 검사까지, 실서버 실행은 사용자 확인 필요)
- 코드를 꼭 고쳐야 하는 건 대형 봇 샤딩 앱의 shard 쿼리(9/15)와 PRUNE_REQUIRES_ADMIN 서버의 정리 권한(8/27) 두 가지뿐이에요
- 파일 기본 한도는 20 MiB로 올랐지만 discord.py 2.7.1의 guild.filesize_limit는 아직 10 MiB를 돌려줘요 → interaction.filesize_limit를 믿으세요
- 문자열 옵션에 줄바꿈이 들어올 수 있으니 splitlines()로 받고, 초대 대상 유저 엔드포인트는 bot.http.request(Route(...))로 직접 호출해요
목차15개 항목
안녕하세요! Mojangcam입니다 😎
지난 #24편에서 11월 16일에 터질 채널 난독화를 미리 밟아봤죠. 그 뒤로 디스코드 개발자 체인지로그에 굵직한 공지가 한 달 사이에 7개나 더 올라왔어요. 하나하나는 짧은데, 모아 놓고 보면 "우리 봇에서 뭘 고쳐야 하지?"가 헷갈리더라고요.
그래서 오늘은 새 기능을 만드는 대신 2026년 8월 말부터 9월까지의 변경을 봇 개발자 눈으로 한 번에 정리할게요. 각 항목마다 "내 봇에 영향이 있나?"와 "코드를 고쳐야 하나?"를 먼저 답하고, discord.py 2.7.1 기준 대응 코드를 붙였어요. 라이브러리에 아직 없는 기능은 엔드포인트를 직접 부르는 방법으로 메꿨습니다 🛠️
이번 글에서 배울 것
- 9월 변경 7가지를 표 하나로 보는 법 (고칠 것 / 안 고쳐도 되는 것)
- 파일 한도가 20 MiB로 올랐는데 라이브러리는 아직 10 MiB라고 말하는 함정
- 슬래시 커맨드 문자열 옵션에 줄바꿈이 들어오기 시작한 뒤의 처리법
- discord.py에 없는 초대 대상 유저(target user) 엔드포인트를
Route로 직접 호출하기 - 대형 봇 샤딩 앱의
shard쿼리,PRUNE_REQUIRES_ADMIN권한 체크 - 연령 확인(Age Assurance)·프로필 비공개로 커스텀 상태가 안 보일 때 안전하게 읽기
📌 한눈에 보기 - 9월 변경 7가지
| 날짜 | 변경 | 내 봇에 영향? | 코드 수정? |
|---|---|---|---|
| 8/27 | 서버 기능 PRUNE_REQUIRES_ADMIN (Breaking) |
멤버 정리(prune) 기능을 쓰는 봇만 | 권한 체크 추가 |
| 9/3 | 파일 업로드 기본 한도 10 MiB → 20 MiB | 파일을 보내는 봇 전부 (좋은 쪽으로) | 한도 계산 코드가 있으면 |
| 9/11 | 슬래시 커맨드: 문자열 옵션 여러 줄 입력, 명령어 퍼지 검색, 채널 이동 시 명령 기억 | 문자열 옵션을 받는 봇 전부 | 줄바꿈 처리만 |
| 9/15 | 대형 봇 샤딩 앱은 GET /users/@me/guilds에 shard 필수 (Breaking) |
대형 봇 샤딩 앱만 | 필수 (해당 시) |
| 9/17 | 프로필 비공개 설정이면 커스텀 상태가 Presence에서 빠짐 | 상태를 읽는 봇 | None 처리 확인 |
| 9/18 | 초대 대상 유저 엔드포인트 4개 추가 | 초대 링크를 만드는 봇 (선택) | 쓰고 싶으면 |
| 9/22 | 연령 확인(Age Assurance) 전 세계 적용 | 없음 (동작 변화 관찰만) | 없음 |
결론부터 말하면 꼭 고쳐야 하는 건 두 개뿐이에요. 대형 봇 샤딩을 쓰는 앱의 shard 쿼리, 그리고 멤버 정리 기능이 있는 봇의 권한 체크. 나머지는 알고 있으면 되는 변화거나, 오히려 봇이 편해지는 변화예요 😌
📌 준비물
- Python 3.8 이상 (예제는 Python 3.13에서 문법 검사했어요)
- discord.py 2.7.1 — 2026-09-30 기준 최신 버전이에요. 3월 이후 새 버전이 없어서 오늘 다루는 9월 변경은 라이브러리에 아직 반영되지 않았습니다
- python-dotenv — 토큰을 .env로 분리하기 위해 필요해요
- 봇을 초대해 둔 테스트 서버 (관리자 권한이면 편해요)
pip install discord.py==2.7.1 python-dotenv프로젝트 폴더에 .env 파일을 만들고 토큰을 넣어주세요.
DISCORD_TOKEN=여기에_봇_토큰💡 .env는 꼭 .gitignore에 추가하세요. 깃허브에 토큰이 올라가면 디스코드가 자동으로 감지해서 토큰을 폐기해버려요 😱
오늘 예제는 커스텀 상태를 읽는 부분 때문에 개발자 포털 → Bot 탭에서 Server Members Intent와 Presence Intent를 켜야 해요. 상태 읽기가 필요 없으면 intents.presences = True 줄을 지우고 포털 설정도 건너뛰어도 됩니다.
📌 1. 파일 업로드 기본 한도 20 MiB (9/3)
변경 내용 : 기본 파일 업로드 한도가 10 MiB에서 20 MiB로 올랐어요. 사용자뿐 아니라 봇·웹훅·인터랙션 응답에도 똑같이 적용됩니다.
내 봇에 영향? : 파일을 보내는 봇이라면 그냥 좋은 소식이에요. 부스트 없는 서버에서 15 MB짜리 로그 파일이나 이미지가 이제 올라갑니다.
그런데 함정이 하나 있어요. discord.py 2.7.1 소스를 열어 보면 기본 한도가 아직 옛날 값이에요.
# discord/utils.py (2.7.1)
DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760 # 10 MiB그래서 guild.filesize_limit나 ctx.filesize_limit는 부스트 없는 서버에서 여전히 10 MiB를 돌려줍니다. "한도보다 크면 안 보내기" 같은 방어 코드를 이 값으로 짰다면, 실제로는 보낼 수 있는 파일을 봇이 스스로 거절하고 있는 거예요 😅
정답은 디스코드가 인터랙션마다 실어 보내는 값을 쓰는 거예요.
@bot.tree.command(name="파일한도", description="이 서버에서 봇이 올릴 수 있는 파일 크기를 보여줘요")
async def file_limit(interaction: discord.Interaction):
live = interaction.filesize_limit # 디스코드가 이번 상호작용에 실어 보낸 값
cached = interaction.guild.filesize_limit if interaction.guild else None # 라이브러리 표
msg = f"디스코드가 알려준 한도: **{live / MIB:.0f} MiB**"
if cached is not None:
msg += f"\ndiscord.py 표 기준(guild.filesize_limit): {cached / MIB:.0f} MiB"
await interaction.response.send_message(msg, ephemeral=True)하나씩 뜯어볼게요 💡
- interaction.filesize_limit : discord.py 2.6에서 추가된 속성이에요. 인터랙션 페이로드의
attachment_size_limit값을 그대로 담아서, 라이브러리 표가 아니라 디스코드 서버가 지금 알려주는 한도예요. 20 MiB 변경이 여기엔 바로 반영됩니다. - guild.filesize_limit : 라이브러리 안에 하드코딩된 부스트 단계별 표를 봐요. 2.7.1에서는 0·1단계가 10 MiB, 2단계 50 MiB, 3단계 100 MiB입니다. 다음 버전에서 고쳐지기 전까지는 참고용으로만 보세요.
- MIB = 1024 * 1024 : 디스코드 문서는 MB가 아니라 MiB(1,048,576바이트) 단위예요. 20 MB(20,000,000바이트)로 계산하면 약 1 MB 손해 봅니다.
📌 2. 슬래시 커맨드 개선 - 여러 줄 입력·퍼지 검색 (9/11)
변경 내용 세 가지예요.
- 문자열 옵션에 여러 줄 입력이 돼요. 사용자가 Shift+Enter로 줄을 바꿔서 보낼 수 있습니다.
- 명령어 고르기 창이 퍼지 검색을 지원해요.
/r a만 쳐도/role add가 뜹니다. - 채널을 옮겨도 고르던 명령어를 기억해요.
내 봇에 영향? : 1번이 핵심이에요. 지금까지 문자열 옵션은 한 줄이라고 믿어도 됐는데, 이제 \n이 섞인 값이 들어옵니다. 옵션 값을 그대로 임베드 제목에 넣거나, 파일 이름·DB 키로 쓰거나, 한 줄 로그로 남기던 코드가 있다면 모양이 깨질 수 있어요.
처리는 간단해요. 줄 단위로 나눠서 받는 습관만 들이면 됩니다.
@bot.tree.command(name="여러줄", description="여러 줄로 입력한 글을 줄 단위로 나눠서 보여줘요")
@app_commands.describe(text="Shift+Enter 로 줄을 바꿔서 입력해 보세요")
async def multiline(interaction: discord.Interaction, text: app_commands.Range[str, 1, 1000]):
lines = [line.strip() for line in text.splitlines() if line.strip()]
if not lines:
await interaction.response.send_message("내용이 비어 있어요!", ephemeral=True)
return
body = "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
await interaction.response.send_message(f"총 **{len(lines)}줄** 받았어요.\n{body}")하나씩 뜯어볼게요 💡
- text.splitlines() :
\n,\r\n어느 쪽이 와도 줄 단위로 잘라줘요.split("\n")보다 안전합니다. - if line.strip() : 빈 줄을 버려요. 여러 줄 입력이 되면 사용자가 Enter를 두 번 누르는 경우가 꼭 생기거든요.
- app_commands.Range[str, 1, 1000] : 문자열 길이 상한이에요. 여러 줄이 되면서 긴 글이 들어오기 쉬워졌으니 상한을 명시해 두는 게 좋아요. 디스코드 메시지 한 개는 2,000자까지라, 1,000자로 잡으면 번호를 붙여 되돌려줘도 안전합니다.
- 한 줄이어야 하는 곳 : 제목·파일 이름·키로 쓸 값은
text.replace("\n", " ")로 한 줄로 눌러서 쓰세요.
2번 퍼지 검색은 명령어 이름을 고르는 클라이언트 쪽 기능이에요. #22편에서 만든 옵션 자동완성 콜백이 저절로 퍼지 검색으로 바뀌는 건 아닙니다. 다만 사용자는 이제 "대충 쳐도 찾아주는" 경험에 익숙해질 테니, 자동완성 필터를 startswith처럼 빡빡하게 두지 말고 #22처럼 in(부분 일치)으로 느슨하게 두는 걸 추천해요 🔍
📌 3. 대형 봇 샤딩 앱은 shard 쿼리 필수 (9/15, Breaking)
변경 내용 : 대형 봇 샤딩(large bot sharding) 을 쓰는 앱이 GET /users/@me/guilds(내 봇이 들어간 서버 목록)를 부를 때 shard 쿼리를 반드시 넣어야 해요. 없으면 400 Bad Request가 옵니다. 값은 0부터 max_concurrency - 1까지고, max_concurrency는 Get Gateway Bot 응답의 session_start_limit에 들어 있어요.
내 봇에 영향? : 대형 봇 샤딩은 디스코드가 서버 수가 아주 많은 봇에 따로 승인해 주는 방식이에요. 이 시리즈에서 만드는 규모의 봇은 해당 없습니다. 공식 문서도 "대형 봇 샤딩을 쓰지 않는 앱은 영향 없다"고 못 박았어요. 안심하세요 😌
그래도 해당되는 분을 위해 적어둘게요. discord.py 2.7.1의 client.fetch_guilds()는 shard 파라미터가 없어서 (내부적으로 limit·before·after·with_counts만 보내요) 엔드포인트를 직접 불러야 합니다.
async def fetch_all_guilds_by_shard(client: discord.Client) -> list:
"""대형 봇 샤딩을 쓰는 앱용. 일반 봇은 그냥 client.fetch_guilds() 를 쓰면 돼요."""
_, _, start_limit = await client.http.get_bot_gateway()
max_concurrency = start_limit["max_concurrency"]
guilds = []
for shard in range(max_concurrency):
route = Route("GET", "/users/@me/guilds")
data = await client.http.request(route, params={"shard": shard, "limit": 200})
guilds.extend(data)
return guilds하나씩 뜯어볼게요 💡
- client.http.get_bot_gateway() :
GET /gateway/bot을 호출해서(shards, url, session_start_limit)세 개를 돌려줘요. 세 번째 딕셔너리의max_concurrency가 필요한 값입니다. - Route("GET", "/users/@me/guilds") :
discord.http.Route는 라이브러리가 모든 요청에 쓰는 클래스예요.from discord.http import Route로 가져옵니다. - client.http.request(route, params=...) : 쿼리 문자열은
params로 넘겨요. 레이트리밋 처리는 라이브러리가 해줍니다. 반환값은 파싱된 JSON(서버 딕셔너리 목록)이에요. - 문서의 안내 : 전체 목록이 필요하면 모든 shard 값을 돌면서 합치라고 되어 있어요. 그래서
range(max_concurrency)로 돕니다.
⚠️ bot.http는 공개 API가 아니라 라이브러리 내부 객체예요. 다음 버전에서 시그니처가 바뀔 수 있으니, discord.py가 정식 메서드를 넣어주면 그쪽으로 갈아타세요.
📌 4. 커스텀 상태 숨김(9/17) + 연령 확인 Age Assurance(9/22)
두 개를 묶은 이유는 결과가 같아서예요. 둘 다 "봇이 보던 사용자 정보가 어느 날부터 안 보인다"입니다.
9/17 변경 내용 : 사용자의 프로필 공개 설정이 친구만이거나, 친구와 작은 서버만인데 서버 멤버가 200명을 넘으면, 그 서버로 가는 Presence Update의 activities에서 커스텀 상태가 빠집니다. Playing·Streaming·Listening·Watching·Competing 같은 다른 활동은 그대로예요.
9/22 변경 내용 : 연령 확인이 전 세계로 롤아웃돼요. 공식 문서의 표현을 그대로 옮기면 "앱은 그대로 동작하지만" 네 가지가 달라 보일 수 있어요.
- 프로필 비공개 사용자는 Presence에서 커스텀 상태가 보이지 않을 수 있음 (위 9/17과 같은 이야기)
- 연령 제한이 걸린 Activity는 사용자에게 연령 그룹 확인을 요구할 수 있음
- 부모가 설정한 지출 한도 때문에 일부 구매가 실패할 수 있음
- Social SDK로 보내는 DM이 메시지 요청함으로 더 자주 분류됨
내 봇에 영향? : 코드를 고칠 건 없어요. 대신 member.activities에서 커스텀 상태를 꺼내 쓰는 코드가 있다면 없을 때를 정상 케이스로 다뤄야 합니다. 예전에는 "상태를 설정한 사람이면 반드시 있다"고 가정해도 됐지만, 이제는 아니에요.
@bot.tree.command(name="상태보기", description="멤버의 커스텀 상태를 읽어요 (안 보일 수도 있어요)")
async def show_status(interaction: discord.Interaction, member: discord.Member):
custom = next((a for a in member.activities if isinstance(a, discord.CustomActivity)), None)
if custom is None:
text = "커스텀 상태가 없거나, 프로필 비공개 설정으로 봇에게는 안 보여요."
else:
text = f"{custom.emoji or ''} {custom.name or ''}".strip() or "(이모지만 있는 상태)"
await interaction.response.send_message(f"{member.display_name}: {text}", ephemeral=True)하나씩 뜯어볼게요 💡
- member.activities : 튜플이에요. 커스텀 상태·게임·스포티파이가 섞여 있어서
isinstance(a, discord.CustomActivity)로 골라냅니다.member.activity(단수)는 첫 번째 것만 주니까 커스텀 상태를 찾을 땐 쓰지 마세요. - next(..., None) : 없으면 None이에요. 이 None이 이제 "설정 안 함"과 "숨겨짐" 두 경우를 다 뜻하는데, 봇 입장에서는 구분할 방법이 없어요. 그래서 안내 문구를 둘 다 포함하게 썼어요.
- custom.name / custom.emoji : 둘 다 Optional이에요. 이모지만 있는 상태도 있어서
or ''로 감쌌습니다. - Presence Intent :
member.activities가 채워지려면intents.presences = True와 개발자 포털의 Presence Intent가 둘 다 켜져 있어야 해요. 안 켜면 항상 빈 튜플이라 "숨겨짐"과 구분이 안 됩니다.
📌 5. 초대 대상 유저(target user) 엔드포인트 (9/18)
변경 내용 : 초대 링크에 "이 사람들만 쓸 수 있음" 목록을 붙이는 엔드포인트 4개가 생겼어요.
| 동작 | 메서드·경로 | 본문 |
|---|---|---|
| 대상 추가 | PUT /invites/{invite.code}/target-users/{user.id} |
없음 |
| 대상 제거 | DELETE /invites/{invite.code}/target-users/{user.id} |
없음 |
| 여러 명 추가 | POST /invites/{invite.code}/target-users/bulk-add |
user_ids 배열 (최대 1,000명) |
| 여러 명 제거 | POST /invites/{invite.code}/target-users/bulk-delete |
user_ids 배열 (최대 1,000명) |
네 개 모두 초대를 만든 사람이거나 MANAGE_GUILD(서버 관리) 권한이 있어야 하고, 성공하면 204(본문 없음) 이 옵니다. 체인지로그에는 초대를 만들 때부터 target_user_ids 배열을 함께 넘길 수 있다고도 적혀 있어요.
내 봇에 영향? : 안 써도 아무 문제 없어요. 다만 #33편 파티방처럼 "초대한 사람만 들어오는" 기능을 만들 때, 지금까지는 채널 권한 덮어쓰기로 막았다면 이제 초대 링크 자체를 특정인 전용으로 만들 수 있게 된 거예요.
discord.py 2.7.1의 create_invite()에는 이 옵션이 없어요. (target_user라는 파라미터가 있긴 한데, 그건 방송 보기 초대의 스트리머를 뜻하는 다른 기능이에요. 헷갈리기 딱 좋죠 😅) 그래서 초대는 평소처럼 만들고, 대상 지정만 직접 호출합니다.
@bot.tree.command(name="초대대상", description="특정 사람만 쓸 수 있는 초대 링크를 만들어요")
@app_commands.describe(member="초대 링크를 쓸 수 있게 할 멤버")
async def invite_target(interaction: discord.Interaction, member: discord.Member):
channel = interaction.channel
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("텍스트 채널에서 써주세요!", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
invite = await channel.create_invite(max_age=3600, max_uses=1, unique=True,
reason=f"{member} 전용 초대")
# discord.py 2.7.1 에는 아직 메서드가 없어서 엔드포인트를 직접 호출해요
route = Route("PUT", "/invites/{code}/target-users/{user_id}",
code=invite.code, user_id=member.id)
await bot.http.request(route) # 성공하면 204 (본문 없음)
await interaction.followup.send(
f"{member.mention} 전용 초대 링크를 만들었어요 (1시간, 1회): {invite.url}", ephemeral=True
)하나씩 뜯어볼게요 💡
- channel.create_invite(max_age=3600, max_uses=1, unique=True) : 1시간짜리 1회용 초대예요.
unique=True라서 기존 초대를 재사용하지 않고 새로 만듭니다. 이 초대의 생성자가 우리 봇이니까 봇이 곧 "초대를 만든 사람" 이 되어 대상 지정 권한을 갖게 돼요. - Route("PUT", "/invites/{code}/target-users/{user_id}", code=..., user_id=...) : 경로의
{code}·{user_id}는 키워드 인자로 채워져요. 위 코드로 만들어지는 주소는https://discord.com/api/v10/invites/<코드>/target-users/<유저ID>입니다. (직접 찍어서 확인했어요.) - await bot.http.request(route) : 본문이 없는 PUT이에요. 여러 명이면
Route("POST", "/invites/{code}/target-users/bulk-add", code=...)에json={"user_ids": [id1, id2]}를 넘기면 됩니다. - defer 먼저 : 초대 생성 + 대상 지정으로 HTTP 요청이 두 번이라 3초를 넘길 수 있어요.
defer()뒤에followup.send()로 답합니다.
📌 6. 멤버 정리(prune)에 관리자 권한 (8/27, Breaking)
9월 글이지만 8월 마지막 주 변경이라 같이 챙겨요. 놓치기 쉬운 Breaking Change예요.
변경 내용 : 서버 주인이 PRUNE_REQUIRES_ADMIN 이라는 서버 기능을 켤 수 있게 됐어요. 이게 켜진 서버에서는 정리 예상 인원 조회(Get Guild Prune Count) 와 정리 실행(Begin Guild Prune) 에 ADMINISTRATOR 권한이 필요합니다. 안 켜진 서버는 지금처럼 MANAGE_GUILD + KICK_MEMBERS예요.
내 봇에 영향? : guild.prune_members()나 guild.estimate_pruned_members()를 쓰는 봇만요. 어느 날 갑자기 403 Forbidden이 나기 시작했다면 서버 주인이 이 기능을 켠 거예요.
@bot.tree.command(name="정리예상", description="30일 동안 활동 없는 멤버가 몇 명인지 계산해요 (실제로 내보내지 않아요)")
@app_commands.describe(days="며칠 동안 활동이 없으면 대상으로 볼지 (1~30)")
async def prune_estimate(interaction: discord.Interaction, days: app_commands.Range[int, 1, 30] = 30):
guild = interaction.guild
if guild is None:
await interaction.response.send_message("서버 안에서 써주세요!", ephemeral=True)
return
perms = guild.me.guild_permissions
if "PRUNE_REQUIRES_ADMIN" in guild.features:
if not perms.administrator:
await interaction.response.send_message(
"이 서버는 **PRUNE_REQUIRES_ADMIN** 이 켜져 있어서 봇에 관리자 권한이 필요해요.",
ephemeral=True,
)
return
elif not (perms.kick_members and perms.manage_guild):
await interaction.response.send_message(
"봇에 **멤버 추방 + 서버 관리** 권한이 있어야 계산할 수 있어요.", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True)
count = await guild.estimate_pruned_members(days=days)
await interaction.followup.send(f"{days}일 기준 정리 대상: **{count}명** (계산만 했어요)", ephemeral=True)하나씩 뜯어볼게요 💡
- guild.features : 서버가 켜둔 기능 이름의 문자열 목록이에요.
"COMMUNITY","NEWS"같은 값이 들어 있고, 이번에"PRUNE_REQUIRES_ADMIN"이 추가됐어요. 라이브러리가 새 값을 몰라도 문자열 그대로 오기 때문에 2.7.1에서도 바로 확인됩니다. - guild.me.guild_permissions : 서버 안에서 우리 봇의 최종 권한이에요.
administrator·kick_members·manage_guild를 여기서 봅니다. - estimate_pruned_members(days=...) : 실제로 내보내지 않고 숫자만 돌려줘요. 예제에서는 일부러 이쪽만 썼어요. 진짜 내보내는
prune_members()는 되돌릴 수 없으니 확인 버튼을 한 번 더 두는 걸 추천합니다. - 왜 미리 체크하나 : 그냥 호출해도 403이 나면서 실패하긴 해요. 하지만 사용자에게 "봇에 관리자 권한을 주세요"라고 정확한 이유를 알려주는 것과 "오류가 났어요"만 보여주는 건 완전히 다르죠.
💡 8월에 놓쳤을 수 있는 것
- 8/12 채널 난독화 — 11/16부터 봇이 못 보는 채널이 가려져요. #24편에서 자세히 다뤘습니다.
- 8/5 첨부파일 옵션의
file_types—ATTACHMENT옵션과 파일 업로드 컴포넌트에 "이 확장자만 받기" 필터가 생겼어요. discord.py 2.7.1에는 아직 없어서 (소스에file_types가 없어요) 라이브러리 업데이트를 기다려야 합니다. - 8/5 채널의
application_id가 null 가능 — 채널 객체의application_id가 항상 ID라고 가정한 코드가 있으면 None 처리를 넣으세요.
📌 전체 코드 (복붙 OK)
sept_check.py 파일 하나로 위 명령어 5개가 다 돌아가요. 그대로 복사해서 쓰세요!
# discord.py 2.7.1 / Python 3.8+
# 2026년 9월 디스코드 API 변경 점검 봇 (sept_check.py)
import os
import discord
from discord import app_commands
from discord.ext import commands
from discord.http import Route
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN")
if not TOKEN:
raise SystemExit("❌ .env 파일에 DISCORD_TOKEN 이 없어요!")
intents = discord.Intents.default()
intents.members = True # 멤버 정보 (개발자 포털에서 켜야 해요)
intents.presences = True # 커스텀 상태 확인용 (개발자 포털에서 켜야 해요)
bot = commands.Bot(command_prefix="!", intents=intents)
MIB = 1024 * 1024
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"✅ 로그인: {bot.user} / discord.py {discord.__version__}")
# ① 9/11 - 문자열 옵션 여러 줄 입력
@bot.tree.command(name="여러줄", description="여러 줄로 입력한 글을 줄 단위로 나눠서 보여줘요")
@app_commands.describe(text="Shift+Enter 로 줄을 바꿔서 입력해 보세요")
async def multiline(interaction: discord.Interaction, text: app_commands.Range[str, 1, 1000]):
lines = [line.strip() for line in text.splitlines() if line.strip()]
if not lines:
await interaction.response.send_message("내용이 비어 있어요!", ephemeral=True)
return
body = "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
await interaction.response.send_message(f"총 **{len(lines)}줄** 받았어요.\n{body}")
# ② 9/3 - 파일 업로드 기본 한도 20 MiB
@bot.tree.command(name="파일한도", description="이 서버에서 봇이 올릴 수 있는 파일 크기를 보여줘요")
async def file_limit(interaction: discord.Interaction):
live = interaction.filesize_limit # 디스코드가 이번 상호작용에 실어 보낸 값
cached = interaction.guild.filesize_limit if interaction.guild else None # 라이브러리 표
msg = f"디스코드가 알려준 한도: **{live / MIB:.0f} MiB**"
if cached is not None:
msg += f"\ndiscord.py 표 기준(guild.filesize_limit): {cached / MIB:.0f} MiB"
await interaction.response.send_message(msg, ephemeral=True)
# ③ 8/27 - PRUNE_REQUIRES_ADMIN 서버 기능
@bot.tree.command(name="정리예상", description="30일 동안 활동 없는 멤버가 몇 명인지 계산해요 (실제로 내보내지 않아요)")
@app_commands.describe(days="며칠 동안 활동이 없으면 대상으로 볼지 (1~30)")
async def prune_estimate(interaction: discord.Interaction, days: app_commands.Range[int, 1, 30] = 30):
guild = interaction.guild
if guild is None:
await interaction.response.send_message("서버 안에서 써주세요!", ephemeral=True)
return
perms = guild.me.guild_permissions
if "PRUNE_REQUIRES_ADMIN" in guild.features:
if not perms.administrator:
await interaction.response.send_message(
"이 서버는 **PRUNE_REQUIRES_ADMIN** 이 켜져 있어서 봇에 관리자 권한이 필요해요.",
ephemeral=True,
)
return
elif not (perms.kick_members and perms.manage_guild):
await interaction.response.send_message(
"봇에 **멤버 추방 + 서버 관리** 권한이 있어야 계산할 수 있어요.", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True)
count = await guild.estimate_pruned_members(days=days)
await interaction.followup.send(f"{days}일 기준 정리 대상: **{count}명** (계산만 했어요)", ephemeral=True)
# ④ 9/18 - 초대 대상 유저(target user) 엔드포인트
@bot.tree.command(name="초대대상", description="특정 사람만 쓸 수 있는 초대 링크를 만들어요")
@app_commands.describe(member="초대 링크를 쓸 수 있게 할 멤버")
async def invite_target(interaction: discord.Interaction, member: discord.Member):
channel = interaction.channel
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("텍스트 채널에서 써주세요!", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
invite = await channel.create_invite(max_age=3600, max_uses=1, unique=True,
reason=f"{member} 전용 초대")
# discord.py 2.7.1 에는 아직 메서드가 없어서 엔드포인트를 직접 호출해요
route = Route("PUT", "/invites/{code}/target-users/{user_id}",
code=invite.code, user_id=member.id)
await bot.http.request(route) # 성공하면 204 (본문 없음)
await interaction.followup.send(
f"{member.mention} 전용 초대 링크를 만들었어요 (1시간, 1회): {invite.url}", ephemeral=True
)
# ⑤ 9/17·9/22 - 커스텀 상태가 안 보일 수 있어요
@bot.tree.command(name="상태보기", description="멤버의 커스텀 상태를 읽어요 (안 보일 수도 있어요)")
async def show_status(interaction: discord.Interaction, member: discord.Member):
custom = next((a for a in member.activities if isinstance(a, discord.CustomActivity)), None)
if custom is None:
text = "커스텀 상태가 없거나, 프로필 비공개 설정으로 봇에게는 안 보여요."
else:
text = f"{custom.emoji or ''} {custom.name or ''}".strip() or "(이모지만 있는 상태)"
await interaction.response.send_message(f"{member.display_name}: {text}", ephemeral=True)
# ⑥ 9/15 - 대형 봇 샤딩 앱은 shard 쿼리 필수
async def fetch_all_guilds_by_shard(client: discord.Client) -> list:
"""대형 봇 샤딩을 쓰는 앱용. 일반 봇은 그냥 client.fetch_guilds() 를 쓰면 돼요."""
_, _, start_limit = await client.http.get_bot_gateway()
max_concurrency = start_limit["max_concurrency"]
guilds = []
for shard in range(max_concurrency):
route = Route("GET", "/users/@me/guilds")
data = await client.http.request(route, params={"shard": shard, "limit": 200})
guilds.extend(data)
return guilds
bot.run(TOKEN)💡 #19편의 cogs/ 구조를 쓰고 계시다면 명령어 부분만 떼어 cogs/sept_check.py로 옮기셔도 똑같이 동작해요.
📌 실행 결과
python sept_check.py봇이 켜지면 서버에서 명령어를 하나씩 쳐보세요. 전부 나만 보이는(ephemeral) 답이라 테스트 서버가 지저분해지지 않아요.

/파일한도 는 부스트 없는 서버라면 위처럼 두 숫자가 다르게 나와야 정상이에요. 위 줄(20 MiB)이 진짜 한도고, 아래 줄(10 MiB)이 라이브러리 표가 낡았다는 증거예요.

/여러줄 은 입력창에서 Shift+Enter를 눌러 줄을 바꿔 보세요. 봇이 줄 수를 세서 번호를 붙여 돌려줍니다.
[이미지: /초대대상 으로 만든 전용 초대 링크 답장과, 대상이 아닌 다른 계정으로 그 링크를 열었을 때 나오는 안내 화면 (부계정 시점)]
/초대대상 은 링크를 대상이 아닌 계정으로 열어봐야 진짜 확인이 돼요. 대상이 아니면 참여가 막혀야 정상입니다.

⚠️ 솔직하게 말씀드리면, 이 글의 코드는 파이썬 3.13에서 문법·import 검사와 Route 주소 조립까지만 확인했고 실제 서버에서 명령어를 돌려본 건 아니에요. 특히 /초대대상 은 9/18에 새로 생긴 엔드포인트라 실제 응답을 꼭 확인해 보세요. 안 되면 댓글 대신 문의로 알려주시면 글을 고칠게요.
📌 자주 나는 에러
1. discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions — /정리예상
- 원인 : 서버가
PRUNE_REQUIRES_ADMIN을 켰는데 봇에 관리자 권한이 없거나, 안 켠 서버에서 멤버 추방·서버 관리 권한이 빠졌어요. - 해결 : 위 코드처럼
guild.features를 먼저 보고 권한을 안내하세요. 봇 역할에 필요한 권한을 준 뒤 다시 시도합니다.
2. discord.errors.Forbidden: 403 — /초대대상 의 PUT 요청
- 원인 : 대상 유저 지정은 초대를 만든 사람이거나 서버 관리 권한이 있어야 해요. 다른 사람이 만든 초대 코드를 넣었거나 봇에 초대 만들기(Create Invite) 권한이 없으면 실패합니다.
- 해결 : 예제처럼 봇이 직접 만든 초대에만 대상을 붙이거나, 봇 역할에 서버 관리 권한을 주세요.
3. discord.errors.HTTPException: 400 Bad Request — GET /users/@me/guilds
- 원인 : 대형 봇 샤딩 앱이
shard쿼리 없이 서버 목록을 불렀어요. 9/15부터 400이 옵니다. - 해결 :
fetch_all_guilds_by_shard()처럼params={"shard": n}을 넣어0부터max_concurrency - 1까지 돌리세요. 일반 봇은 이 에러가 나지 않아요.
4. /상태보기 가 항상 "커스텀 상태가 없거나..." 만 나와요
- 원인 :
intents.presences = True를 안 넣었거나, 개발자 포털에서 Presence Intent를 안 켰어요. 이러면member.activities가 항상 빈 튜플이에요. - 해결 : 코드와 포털 둘 다 켜고 봇을 재시작하세요. 둘 다 켰는데도 안 보이면 그 사용자가 프로필 비공개(9/17) 인 거라 봇은 볼 수 없어요. 그게 정상입니다.
5. 파일을 보냈는데 413 Payload Too Large / Request entity too large
- 원인 : 20 MiB는 부스트 없는 서버의 기본값이에요. 그보다 큰 파일은 여전히 안 되고, MB(1,000,000)와 MiB(1,048,576)를 섞어 계산하면 경계에서 튕깁니다.
- 해결 : 보내기 전에
os.path.getsize()와interaction.filesize_limit를 비교하세요.guild.filesize_limit(2.7.1 기준 10 MiB)로 비교하면 보낼 수 있는 파일도 거절하니 쓰지 마세요.
6. /초대대상 에서 discord.errors.NotFound: 404 (error code: 10062): Unknown interaction
- 원인 : 3초 안에 응답하지 못했어요. HTTP 요청이 두 번이라 느릴 수 있습니다.
- 해결 : 무거운 작업 전에
await interaction.response.defer()를 먼저 부르고, 그 뒤에는interaction.followup.send()로 보내세요.
💬 오늘 꼭 기억할 3가지
- 고쳐야 하는 건 둘. 대형 봇 샤딩 앱의
shard쿼리, 멤버 정리 봇의PRUNE_REQUIRES_ADMIN권한 체크. 나머지는 알고만 있으면 돼요. - 파일 한도는
interaction.filesize_limit. 라이브러리 표(guild.filesize_limit)는 아직 10 MiB로 낡았어요. - 문자열 옵션에 줄바꿈이 들어와요.
splitlines()로 받고, 한 줄이어야 하는 곳은 눌러서 쓰세요. 커스텀 상태는 없는 게 정상 케이스가 됐고요.
📌 마무리
오늘은 새 기능 대신 9월 한 달치 디스코드 API 변경을 한 번에 훑었어요 🗞️
정리하면, 파일 한도가 20 MiB로 넉넉해졌고(라이브러리 값은 믿지 말 것), 슬래시 커맨드 문자열에 여러 줄이 들어오기 시작했고, 초대 링크를 특정인 전용으로 만드는 엔드포인트가 생겼어요. 연령 확인과 프로필 비공개 때문에 커스텀 상태는 안 보이는 경우가 늘었고, 대형 봇 샤딩 앱은 shard 쿼리를, 멤버 정리 봇은 PRUNE_REQUIRES_ADMIN을 챙겨야 합니다. discord.py 2.7.1은 3월 이후 새 버전이 없어서 새 엔드포인트는 Route로 직접 부르는 방법으로 메꿨어요 ✅
그리고 잊지 마세요. 11월 16일 채널 난독화가 제일 큰 변경이에요. 아직 #24편의 /채널점검을 안 돌려보셨다면 이번 주말에 한 번 돌려두세요 💣
디스코드 봇 만들기 시리즈는 #33 파티방에 이어 다음 편에서 봇을 Docker로 배포하기(예정)를 다뤄볼 예정이에요. 기대해주세요! 🙌
