본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 21. 11:25

나에게 각각 일주일의 시간을 뺏었던 Discord.js의 주의사항들 (당신은 그러지 않도록)

요약

Discord.js를 사용하여 봇을 개발할 때 흔히 발생하는 권한 계층 문제와 상호작용 시간 초과 문제를 다룹니다. 역할(Role)의 계층 구조에 따른 권한 제한과 슬래시 커맨드의 3초 응답 제한을 해결하기 위한 실무적인 가이드를 제공합니다.

핵심 포인트

  • 권한(Permission)이 있더라도 봇의 역할(Role) 위치가 대상보다 낮으면 동작하지 않으므로 역할 계층을 확인해야 합니다.
  • 슬래시 커맨드는 3초 이내에 응답해야 하며, LLM 호출 등 시간이 걸리는 작업 시에는 반드시 deferReply()를 사용해야 합니다.
  • 권한 오류(50013) 발생 시 권한 설정뿐만 아니라 서버 설정 내 역할 순서를 점검하는 것이 중요합니다.
  • 지연 응답(deferred reply) 패턴을 사용하여 상호작용 만료(10062) 에러를 방지할 수 있습니다.

저는 지난 1년 동안 500개 이상의 라이브 봇을 배포한 AI Discord 봇 빌더를 운영하고 있습니다. 이 서비스는 평이한 영어 설명을 입력받아 discord.js 코드를 생성하고, 30초 이내에 컨테이너를 프로덕션(Production) 환경으로 배포합니다. 듣기에는 깔끔해 보이지만, 실제로는 그렇지 않습니다. 제가 배운 대부분의 내용은 누군가의 봇이 메시지에 응답하지 않거나, 문서화되지 않은 Discord의 동작 방식 때문에 커뮤니티가 침묵에 빠진 후 새벽 3시에 로그를 확인하며 얻은 것들입니다. 제가 첫날에 이 리스트를 가지고 있었더라면 좋았을 것들을 정리했습니다.

권한 누락 (Missing Permissions, 50013)은 대개 권한의 문제가 아니라 역할(Role) 위치의 문제입니다. 제가 처음 이 문제를 겪었을 때, 봇의 권한 정수(Permission integer)를 감사하는 데 두 시간을 썼습니다. 권한은 정확했습니다. 하지만 봇은 여전히 아무도 추방(Kick)할 수 없었습니다. 아무도 크게 강조하지 않는 사실은 다음과 같습니다: discord.js의 권한은 역할 계층(Role hierarchy)에 의해 제한됩니다. 봇이 KickMembers 권한을 가지고 있더라도, 대상의 최상위 역할이 봇의 최상위 역할보다 위에 있다면 추방에 실패합니다.

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== 'kick') return;

  const target = interaction.options.getMember('user');
  const botMember = interaction.guild.members.me;

  if (target.roles.highest.position >= botMember.roles.highest.position) {
    return interaction.reply({
      content: "I can't kick that user — their role is at or above mine.",
      ephemeral: true,
    });
  }

  await target.kick(interaction.options.getString('reason') ?? 'No reason');
  await interaction.reply(`Kicked ${target.user.tag}.`);
});

Discord에서 해결 방법: 서버 설정(Server Settings) → 역할(Roles)에서 봇의 역할을 관리해야 하는 역할들보다 위로 드래그하세요. 이것은 "작동하는" 봇이 작동하지 않는 가장 흔한 이유입니다.

알 수 없는 상호작용 (Unknown Interaction, 10062)은 3초간의 경주입니다.
슬래시 커맨드(Slash commands)는 응답할 수 있는 시간을 정확히 3초만 제공합니다. 만약 .reply()를 호출하기 전에 대기(await)가 필요한 DB 쿼리, 제3자 API, 또는 LLM을 호출한다면, 당신은 그 경주에서 패배하게 될 것입니다.

잘못된 예:
const result = await callLLM(prompt); // 2.4s
await interaction.reply(result); // 10062

올바른 예:
await interaction.deferReply(); // 15분의 시간을 벌어줍니다
const result = await callLLM(prompt);
await interaction.editReply(result);

지연 응답 (deferred reply) 패턴은 한 번 제대로 내재화해 두면 다시는 발목을 잡히지 않을 요소 중 하나입니다.

Intent 플래그는 소리 없는 살인자입니다
이 문제는 에러가 발생하지 않기 때문에 매우 치명적입니다. 봇은 연결되고, 온라인 상태로 표시되며, 명령어를 등록하지만, 메시지를 전혀 읽지 못합니다. 두 가지 단계가 필요합니다:

  1. Discord Developer Portal에서 "Message Content Intent"를 활성화합니다.
  2. 클라이언트 설정(client config)에 이를 목록에 추가합니다:

import { Client, GatewayIntentBits } from 'discord.js';
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent, // ← 이것이 없으면 message.content가 비어 있게 됩니다
GatewayIntentBits.GuildMembers,
],
});

두 단계 중 하나라도 건너뛰면, message.content가 빈 문자열로 전달되어 접두사(prefix) 명령어들이 아무런 이유 없이 작동하지 않는 것처럼 보이게 됩니다.

오래된 메시지에 대한 반응(Reactions) 및 수정(edits)은 부분적(partial)입니다
봇이 생성되는 것을 관찰하지 못한 메시지(봇의 세션보다 오래된 메시지)에 누군가 반응을 남기면, 이벤트 페이로드(event payload)는 부분적입니다. fetch()를 통해 가져오기 전까지 .content를 호출하면 null을 반환합니다.

client.on('messageReactionAdd', async (reaction, user) => {
if (reaction.partial) {
try {
await reaction.fetch();
} catch {
return;
}
}
if (reaction.message.partial) {
try {
await reaction.message.fetch();
} catch {
return;
}
}
// 이제 reaction.message.content를 안전하게 사용할 수 있습니다
});

Partials 설정도 추가해야 합니다:

import { Client, Partials } from 'discord.js';
new Client({
intents: [...],
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
});

Ephemeral(비공개) 응답은 일반 메시지처럼 동작하지 않습니다
channel.messages.fetch()를 통해 이 메시지들을 가져올 수 없습니다. 또한 반응(reactions)을 추가할 수도 없습니다.

나중에 하나를 업데이트하고 싶다면, 메시지 ID가 아니라 원본 상호작용 토큰 (interaction token)이 필요합니다:

await interaction.reply({ content: 'Loading…', ephemeral: true });
// 10초 후
await interaction.editReply({ content: 'Done.' });

만약 프로세스 간에 상호작용 (interaction)을 직렬화 (serialize) 한다면 (큐 (queue), 웹훅 핸들러 (webhook handler) 등), 메시지 참조 (message reference)가 아닌 토큰을 직렬화하세요.

프로세스 충돌은 게이트웨이 세션 (gateway session)을 방치합니다
웹소켓 (websocket)을 닫지 않고 봇이 강제 종료되면, Discord 측에서는 해당 세션을 몇 분 동안 활성 상태로 유지합니다. 이 경우 새로운 배포 (deploy)가 하루 1,000회 세션 시작 제한에 포함되며, 콜드 재시작 (cold restart) 시 두 개의 봇 인스턴스가 중복 메시지를 게시하는 짧은 구간이 발생할 수 있습니다.

async function shutdown(signal) {
  console.log(`[bot] ${signal} received, closing gateway`);
  try {
    await client.destroy();
  } catch {}
  process.exit(0);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('unhandledRejection', (err) => {
  console.error('[bot] unhandled rejection:', err);
  // 충돌하지 않도록 — 로그를 남기고 계속 진행합니다
});
process.on('uncaughtException', (err) => {
  console.error('[bot] uncaught:', err);
  // 충돌하되, 깔끔하게 종료합니다
  shutdown('uncaughtException');
});

강제 종료 전 SIGTERM을 보내는 컨테이너 플랫폼에서 실행 중이라면, 이 코드 블록 하나가 절대 찾아낼 수 없는 종류의 버그들을 방지해 줍니다.

속도 제한 (Rate limits)이 항상 429 에러인 것은 아닙니다
REST 클라이언트는 소프트 케이스 (soft cases, Discord의 경로별 버킷 경고)에 대해 rateLimited 이벤트를 발생시킵니다. 첫날부터 이를 로그로 남긴다면, 무료 모니터링 신호를 얻는 것과 같습니다:

client.rest.on('rateLimited', (info) => {
  console.warn('[ratelimit]', {
    route: info.route,
    method: info.method,
    timeToReset: info.timeToReset,
    limit: info.limit,
  });
});

운영 환경 (production)에서 이러한 문제는 거의 항상 1만 명 규모의 서버에 있는 특정 봇 하나가 루프 내에서 어리석은 행동을 하고 있는 것으로 추적됩니다. 그렇지 않다면 에러 모니터링에는 절대 나타나지 않습니다.

가장 큰 문제: 대부분의 운영 환경(production)에서의 장애는 당신의 코드에 있는 버그가 아닙니다. 수백 개의 봇을 운영하게 되면 장애의 분포가 달라집니다. 제가 목격한 "봇이 고장 났어요"라는 티켓의 약 30%는 다음과 같은 원인으로 거슬러 올라갑니다:

  • 서버 관리자가 역할(roles)을 변경함
  • 사용자가 봇의 초대(invite)를 취소함
  • 누군가 1,000개의 반응(reactions)을 스크립트로 실행하여 봇이 속도 제한(rate-limited)에 걸림
  • Discord 측에서 동작 방식(behavior)을 변경함
  • 봇이 메시지를 게시하도록 예약되어 있던 채널이 삭제됨

당신의 봇은 이 중 어떤 일이 발생했는지 빠르게 증명할 수 있을 만큼 충분한 로그를 남겨야 합니다. 중요한 이벤트마다 하나의 구조화된 로그(시도된 작업, 길드 ID, 사용자 ID, 결과, 지연 시간(latency))를 남기는 것만으로도 문제 해결의 80%를 달성할 수 있습니다.

저는 vibebot.gg를 운영하며 이 모든 것을 고통스럽게 배웠습니다. 이곳은 사용자가 영어로 Discord 봇을 설명하면 시스템이 그들을 위해 discord.js 코드를 생성하고 배포하는 서비스입니다. 이 설정에서 흥미로운 장애 모드는, 사용자가 코드를 전혀 볼 수 없기 때문에 LLM이 생성한 코드가 처음부터 이 8가지의 주의사항(gotchas)을 모두 올바르게 처리해야 한다는 점입니다. 위에서 언급한 패턴들은 현재 저희가 템플릿 단계에서 생성되는 모든 봇에 그대로 녹여내고 있는 것들입니다. 만약 처음부터 Discord 봇을 구축하고 있다면, 위의 코드 조각(snippets)들을 그대로 복사해서 사용하세요. 각각 일주일의 디버깅 시간을 아껴줄 가치가 있습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0