해당 글은, 제가 작성한 Discord.js 보일러플레이트를 기반으로 합니다. 해당 보일러픝레이트는 다음에라도 봇을 빠르게 만들고 싶으실 때 사용하실 수 있습니다. Discord.js TypeScript Boilerplate
지금까지 우리는 슬래시 명령어의 구조를 잡고, 옵션과 서브커맨드를 활용하여 다양한 기능을 가진 명령어를 만드는 방법을 배웠습니다. 이제 우리 봇은 제법 여러 가지 일을 할 수 있게 되었죠. 하지만 사용자가 너무 짧은 시간 안에 명령어를 반복해서 사용하거나, 예상치 못한 오류가 발생했을 때 봇이 불안정해지거나 서버에 부담을 줄 수 있습니다.
이번 시간에는 이러한 문제들을 방지하고 봇을 더욱 안정적으로 운영하기 위한 두 가지 중요한 주제, 바로 명령어 쿨타임(Cooldown)과 오류 처리(Error Handling)에 대해 깊이 있게 알아보겠습니다.
명령어 남용 방지: 쿨타임 구현하기
쿨타임은 특정 명령어를 한 번 사용한 후, 일정 시간이 지나야만 다시 사용할 수 있도록 제한하는 기능입니다. 예를 들어, 외부 API를 호출하여 정보를 가져오는 명령어의 경우, 너무 잦은 호출은 API 제공처에 부담을 주거나 계정 사용량 제한에 걸릴 수 있습니다. 또는 단순히 특정 명령어가 채팅창을 도배하는 것을 막기 위해서도 쿨타임은 유용합니다.
쿨타임 기본 아이디어
쿨타임을 구현하는 가장 기본적인 방법은 다음과 같습니다.
- 사용자가 명령어를 사용할 때마다, 해당 사용자와 명령어 조합에 대한 마지막 사용 시간을 기록합니다.
- 다음에 같은 사용자가 같은 명령어를 사용하려고 할 때, 현재 시간과 마지막 사용 시간을 비교합니다.
- 미리 설정된 쿨타임 시간보다 짧은 시간 안에 다시 사용하려고 하면, 명령어 실행을 막고 사용자에게 알림을 보냅니다.
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 입니다.`
);
}
코드 설명:
cooldowns
:Collection
을 생성하여 사용자 ID와 마지막 명령어 사용 시간을 저장합니다.COOLDOWN_SECONDS
: 이 명령어의 쿨타임 시간을 초 단위로 설정합니다.execute
함수 시작 시:- 현재 사용자의 ID와 현재 시간을 가져옵니다.
cooldowns
에 해당 사용자 ID가 있는지 확인합니다.- 있다면, 마지막 사용 시간과 현재 시간을 비교하여 쿨타임이 지나지 않았으면 사용자에게 남은 시간을 알리고 명령어 실행을 중단합니다.
- 쿨타임이 지났거나 처음 사용하는 경우,
cooldowns
에 현재 사용 시간을 기록합니다. - (선택 사항)
setTimeout
을 사용하여 쿨타임이 만료된 후에는cooldowns
에서 해당 사용자 정보를 삭제하여 메모리를 절약할 수 있습니다. 이 방식은 사용자가 매우 많을 때 유용할 수 있습니다.
- 이후 기존의 명령어 로직을 실행합니다.
이 방식은 각 명령어 파일마다 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 |