[DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리

2025. 6. 5. 17:11·DiscordJS 개발 튜토리얼

해당 글은, 제가 작성한 Discord.js 보일러플레이트를 기반으로 합니다. 해당 보일러픝레이트는 다음에라도 봇을 빠르게 만들고 싶으실 때 사용하실 수 있습니다. Discord.js TypeScript Boilerplate

지금까지 우리는 슬래시 명령어의 구조를 잡고, 옵션과 서브커맨드를 활용하여 다양한 기능을 가진 명령어를 만드는 방법을 배웠습니다. 이제 우리 봇은 제법 여러 가지 일을 할 수 있게 되었죠. 하지만 사용자가 너무 짧은 시간 안에 명령어를 반복해서 사용하거나, 예상치 못한 오류가 발생했을 때 봇이 불안정해지거나 서버에 부담을 줄 수 있습니다.

이번 시간에는 이러한 문제들을 방지하고 봇을 더욱 안정적으로 운영하기 위한 두 가지 중요한 주제, 바로 명령어 쿨타임(Cooldown)과 오류 처리(Error Handling)에 대해 깊이 있게 알아보겠습니다.

명령어 남용 방지: 쿨타임 구현하기

쿨타임은 특정 명령어를 한 번 사용한 후, 일정 시간이 지나야만 다시 사용할 수 있도록 제한하는 기능입니다. 예를 들어, 외부 API를 호출하여 정보를 가져오는 명령어의 경우, 너무 잦은 호출은 API 제공처에 부담을 주거나 계정 사용량 제한에 걸릴 수 있습니다. 또는 단순히 특정 명령어가 채팅창을 도배하는 것을 막기 위해서도 쿨타임은 유용합니다.

쿨타임 기본 아이디어

쿨타임을 구현하는 가장 기본적인 방법은 다음과 같습니다.

  1. 사용자가 명령어를 사용할 때마다, 해당 사용자와 명령어 조합에 대한 마지막 사용 시간을 기록합니다.
  2. 다음에 같은 사용자가 같은 명령어를 사용하려고 할 때, 현재 시간과 마지막 사용 시간을 비교합니다.
  3. 미리 설정된 쿨타임 시간보다 짧은 시간 안에 다시 사용하려고 하면, 명령어 실행을 막고 사용자에게 알림을 보냅니다.

Discord.js에서는 Collection (JavaScript의 Map과 유사하지만 추가 기능이 있음)을 사용하여 이러한 정보를 효율적으로 관리할 수 있습니다.

쿨타임 적용하기

discordjs_typescript_boilerplate에는 아직 쿨타임 기능이 직접적으로 구현되어 있지 않으므로, 우리가 직접 추가해보겠습니다. src/index.ts 파일이나, 각 명령어 파일 내에서 쿨타임을 관리할 수 있습니다. 여기서는 각 명령어별로 쿨타임을 설정하고 관리하는 방식을 예시로 들어보겠습니다.

먼저, 명령어 파일에 쿨타임 정보를 추가할 수 있도록 구조를 확장해봅시다. 예를 들어, ping.ts 명령어에 5초의 쿨타임을 주고 싶다고 가정해봅시다.

// src/commands/ping.ts
import {
  SlashCommandBuilder,
  ChatInputCommandInteraction,
  Collection,
} from "discord.js";

// 이 명령어에 대한 쿨타임 정보를 저장할 Collection
// Key: 사용자 ID (string), Value: 마지막 사용 시간 (timestamp, number)
const cooldowns = new Collection<string, number>();
const COOLDOWN_SECONDS = 5; // 5초 쿨타임

export const data = new SlashCommandBuilder()
  .setName("핑")
  .setDescription("봇의 응답 속도를 확인합니다.");

export async function execute(interaction: ChatInputCommandInteraction) {
  const userId = interaction.user.id;
  const now = Date.now();

  if (cooldowns.has(userId)) {
    const lastUsage = cooldowns.get(userId)!;
    const expirationTime = lastUsage + COOLDOWN_SECONDS * 1000;

    if (now < expirationTime) {
      const timeLeft = (expirationTime - now) / 1000;
      return interaction.reply({
        content: `너무 빨리 명령어를 사용하셨어요! \\${timeLeft.toFixed(
          1
        )}초 뒤에 다시 시도해주세요.`,
        ephemeral: true, // 사용자에게만 보이는 메시지
      });
    }
  }

  // 쿨타임이 지났거나 처음 사용하는 경우, 현재 시간을 기록
  cooldowns.set(userId, now);
  // 쿨타임이 만료된 후에는 메모리에서 해당 사용자 정보를 삭제 (선택적 최적화)
  setTimeout(() => cooldowns.delete(userId), COOLDOWN_SECONDS * 1000);

  // 기존 명령어 로직
  const sentMessage = await interaction.reply({
    content: "퐁! 응답 속도를 계산하고 있어요...",
    fetchReply: true,
  });
  const latency = sentMessage.createdTimestamp - interaction.createdTimestamp;
  await interaction.editReply(
    `퐁! 🏓 응답 속도는 \\${latency}ms 입니다. API 지연 시간은 약 \\${Math.round(
      interaction.client.ws.ping
    )}ms 입니다.`
  );
}

코드 설명:

  1. cooldowns: Collection을 생성하여 사용자 ID와 마지막 명령어 사용 시간을 저장합니다.
  2. COOLDOWN_SECONDS: 이 명령어의 쿨타임 시간을 초 단위로 설정합니다.
  3. execute 함수 시작 시:
    • 현재 사용자의 ID와 현재 시간을 가져옵니다.
    • cooldowns에 해당 사용자 ID가 있는지 확인합니다.
      • 있다면, 마지막 사용 시간과 현재 시간을 비교하여 쿨타임이 지나지 않았으면 사용자에게 남은 시간을 알리고 명령어 실행을 중단합니다.
    • 쿨타임이 지났거나 처음 사용하는 경우, cooldowns에 현재 사용 시간을 기록합니다.
    • (선택 사항) setTimeout을 사용하여 쿨타임이 만료된 후에는 cooldowns에서 해당 사용자 정보를 삭제하여 메모리를 절약할 수 있습니다. 이 방식은 사용자가 매우 많을 때 유용할 수 있습니다.
  4. 이후 기존의 명령어 로직을 실행합니다.

이 방식은 각 명령어 파일마다 cooldowns Collection과 관련 로직을 추가해야 합니다. 만약 모든 명령어에 일괄적으로 적용하거나, 좀 더 중앙에서 관리하고 싶다면 src/index.ts의 InteractionCreate 이벤트 핸들러에서 처리하는 방법도 있습니다. 이 경우, 어떤 명령어가 호출되었는지, 해당 명령어의 쿨타임은 얼마인지 등의 정보를 추가로 관리해야 합니다.

discordjs_typescript_boilerplate의 src/commands/index.ts에서 각 명령어 모듈이 data, execute 외에 cooldown (숫자, 초 단위) 같은 속성을 추가로 export 하도록 하고, src/index.ts에서 이 값을 읽어와 쿨타임을 적용하는 것도 좋은 방법입니다.

예상치 못한 상황에 대비: 오류 처리

아무리 코드를 꼼꼼하게 작성해도 예상치 못한 오류는 발생할 수 있습니다. 네트워크 문제, 외부 API의 예기치 않은 응답, 코드의 논리적 결함 등 원인은 다양합니다. 중요한 것은 오류가 발생했을 때 봇이 완전히 멈춰버리거나 사용자에게 혼란을 주지 않도록 적절히 처리하는 것입니다.

try...catch 기본

JavaScript와 TypeScript에서 오류를 처리하는 가장 기본적인 방법은 try...catch 구문입니다.

try {
  // 오류가 발생할 가능성이 있는 코드
  // 예를 들어, await someAsyncFunction();
} catch (error) {
  // 오류가 발생했을 때 실행될 코드
  console.error("오류 발생:", error);
  // 사용자에게 알림을 보낼 수도 있습니다.
}

명령어 실행 중 오류 처리

각 명령어의 execute 함수 내부에서 중요한 로직은 try...catch로 감싸는 것이 좋습니다.

// src/commands/어떤명령어.ts
// ... (data 정의) ...
export async function execute(interaction: ChatInputCommandInteraction) {
  try {
    // 명령어의 핵심 로직
    await interaction.reply("명령어가 성공적으로 실행되었습니다!");
  } catch (error) {
    console.error(`[\\${interaction.commandName}] 명령어 실행 중 오류:`, error);

    // 사용자에게 오류를 알립니다.
    // 이미 응답을 보냈는지 (replied) 또는 응답을 수정한 적이 있는지 (deferred) 확인하여
    // 적절한 응답 방식을 사용합니다.
    if (interaction.replied || interaction.deferred) {
      await interaction.followUp({
        content: "명령을 처리하는 동안 문제가 발생했어요. 😥",
        ephemeral: true,
      });
    } else {
      await interaction.reply({
        content: "명령을 처리하는 동안 문제가 발생했어요. 😥",
        ephemeral: true,
      });
    }
  }
}

코드 설명:

  • execute 함수의 주요 로직을 try 블록 안에 넣습니다.
  • 오류 발생 시 catch 블록에서 console.error로 서버 로그에 오류를 기록합니다. 이때 어떤 명령어에서 오류가 발생했는지 함께 기록하면 디버깅에 도움이 됩니다.
  • 사용자에게도 오류가 발생했음을 알립니다.
    • interaction.replied 또는 interaction.deferred 속성을 확인하여, 이미 봇이 한 번 응답을 했거나 응답을 지연시킨 상태인지 체크합니다.
    • 이미 응답한 상태라면 interaction.followUp()을 사용하여 추가 메시지를 보내고, 그렇지 않다면 interaction.reply()를 사용합니다.
    • ephemeral: true 옵션으로 오류 메시지는 명령어를 사용한 당사자에게만 보이도록 합니다.

전역 오류 처리 (src/index.ts)

discordjs_typescript_boilerplate의 src/index.ts 파일에 있는 InteractionCreate 이벤트 핸들러에도 이미 try...catch 블록이 있습니다. 이는 개별 명령어에서 미처 처리하지 못한 오류나, 명령어 자체를 찾는 과정에서의 오류 등을 잡아내는 최후의 방어선 역할을 합니다.

// src/index.ts (일부 발췌)
client.on(Events.InteractionCreate, async (interaction) => {
  try {
    if (!interaction.isChatInputCommand()) return;
    const command = commands[interaction.commandName as keyof typeof commands];
    if (!command) {
      // ... 명령어를 찾지 못한 경우 처리 ...
      return;
    }
    await command.execute(interaction); // 여기서 발생한 오류가 command.execute 내부에서 처리되지 않으면 아래 catch로 넘어감
  } catch (error) {
    console.error("전역 인터랙션 핸들러 오류:", error);
    if (interaction.isRepliable()) {
      // isRepliable()은 replied 또는 deferred가 false일 때 true
      // 이미 응답했는지 여부에 따라 reply 또는 followUp을 선택하는 로직이 더 안전할 수 있습니다.
      const replyMethod =
        interaction.replied || interaction.deferred ? "followUp" : "reply";
      await interaction[replyMethod]({
        content: "명령어 처리 중 알 수 없는 오류가 발생했습니다.",
        ephemeral: true,
      }).catch((e) => console.error("오류 응답 전송 실패:", e)); // 오류 응답 전송 자체도 실패할 수 있음
    }
  }
});

이 전역 핸들러는 모든 명령어 실행을 감싸고 있기 때문에, 각 명령어 파일에서 try...catch를 잘 구현했더라도 이중으로 안전장치를 마련하는 효과가 있습니다.

특정 오류에 대한 구체적인 처리

때로는 발생할 수 있는 특정 오류 유형을 미리 알고 있고, 그에 따라 다른 방식으로 처리하고 싶을 수 있습니다. 예를 들어, 디스코드 API 권한 부족으로 인한 오류(DiscordAPIError의 특정 코드)가 발생하면 사용자에게 "봇에게 필요한 권한이 없어요!"라고 알려줄 수 있습니다.

import { DiscordAPIError } from 'discord.js';

// ... execute 함수 내에서 ...
} catch (error) {
    if (error instanceof DiscordAPIError && error.code === 50013) { // 50013: Missing Permissions
        console.warn(`[\\${interaction.commandName}] 권한 부족 오류: \\${error.message}`);
        await interaction.reply({ content: '이 명령을 실행하기 위한 봇의 권한이 부족해요. 서버 관리자에게 문의해주세요.', ephemeral: true });
    } else {
        console.error(`[\\${interaction.commandName}] 명령어 실행 중 오류:`, error);
        // 일반적인 오류 메시지
        const replyMethod = interaction.replied || interaction.deferred ? 'followUp' : 'reply';
        await interaction[replyMethod]({ content: '명령을 처리하는 동안 문제가 발생했어요.', ephemeral: true });
    }
}

마무리

이번 시간에는 명령어 쿨타임을 설정하여 봇의 남용을 방지하는 방법과, try...catch를 활용하여 예상치 못한 오류에 효과적으로 대응하는 방법을 배웠습니다. 이러한 기능들은 사용자 경험을 향상시키고 봇을 더욱 안정적이고 견고하게 만드는 데 필수적입니다.

다음 시간에는 봇의 응답을 더욱 풍부하고 보기 좋게 만들어주는 임베드(Embed) 메시지와, 사용자와의 상호작용을 한 단계 끌어올릴 수 있는 버튼(Button) 컴포넌트를 만드는 방법에 대해 알아보겠습니다. 봇과의 대화가 더욱 즐거워질 거예요!

'DiscordJS 개발 튜토리얼' 카테고리의 다른 글

[DiscordJS 봇 개발 튜토리얼] 5. 임베드 메시지와 버튼 만들기: 봇과의 소통을 더 풍부하게!  (0) 2025.06.06
[DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!  (1) 2025.06.04
[DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음  (1) 2025.06.03
[DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기  (2) 2025.06.02
[DiscordJS 봇 개발 튜토리얼] 0. 프로젝트, 봇 생성하기  (3) 2025.06.01
'DiscordJS 개발 튜토리얼' 카테고리의 다른 글
  • [DiscordJS 봇 개발 튜토리얼] 5. 임베드 메시지와 버튼 만들기: 봇과의 소통을 더 풍부하게!
  • [DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!
  • [DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음
  • [DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기
디스호스트
디스호스트
쉽고 안정적인 디스코드 봇 호스팅 서비스, 디스호스트의 기술 블로그입니다. 디스호스트는 24시간 구동되는 서버를 통해 디스코드 봇을 대신 구동시켜 드리는 서비스를 제공하고 있습니다.
  • 디스호스트
    디스호스트 기술 블로그
    디스호스트
  • 블로그 메뉴

    • 홈
    • 디스호스트 사용 가이드
    • 디스코드 봇 호스팅, 24시간 서버 구동
    • 분류 전체보기 (33) N
      • 디스코드 (6)
      • 디스호스트 가이드 (11)
      • 봇 개발 팁 (10) N
        • Discord.js (8)
        • Discord.py (1) N
      • DiscordJS 개발 튜토리얼 (6) N
  • 링크

    • 디스호스트
    • 디스호스트 패널
  • hELLO· Designed By정상우.v4.10.3
디스호스트
[DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리
상단으로

티스토리툴바