해당 글은, 제가 작성한 Discord.js 보일러플레이트를 기반으로 합니다. 해당 보일러픝레이트는 다음에라도 봇을 빠르게 만들고 싶으실 때 사용하실 수 있습니다. Discord.js TypeScript Boilerplate
지난 시간에는 우리 봇에게 간단한 상태 메시지를 설정해서 조금이나마 생기를 불어넣어 봤습니다. 아직 우리 봇은 "핑"이라고 말을 걸면 "퐁!"하고 대답하는 정도의 아주 기본적인 기능만 가지고 있죠. 봇의 기능이 점점 많아지면 index.ts
파일 하나에 모든 코드를 다 넣는 건 좋은 생각이 아닙니다. 코드가 길어지고 복잡해지면 관리하기가 여간 어려운 게 아니거든요.
그래서 이번 시간에는 앞으로 우리가 만들 다양한 명령어들을 효과적으로 관리할 수 있도록 '명령어 구조'를 잡아보는 시간을 갖겠습니다. 특히 이 구조는 다음 시간에 다룰 '슬래시 명령어'를 깔끔하게 처리하기 위한 중요한 밑거름이 될 겁니다.
왜 명령어 구조를 잡아야 할까요?
봇을 처음 만들 때는 한두 가지 간단한 명령어로 시작하겠지만, 기능을 하나둘 추가하다 보면 index.ts
파일이 금방 수백, 수천 줄로 늘어날 수 있습니다. 이렇게 되면 다음과 같은 문제들이 생길 수 있어요.
- 가독성 저하: 원하는 코드를 찾기가 너무 힘들어집니다.
- 유지보수 어려움: 코드 한 부분을 수정했을 때 다른 부분에 어떤 영향을 줄지 파악하기 어렵습니다.
- 협업의 걸림돌: 여러 명이 함께 봇을 개발한다면, 파일 하나를 계속 같이 수정하는 건 충돌의 연속일 수 있습니다.
- 확장성 문제: 새로운 명령어를 추가할 때마다
index.ts
파일을 건드려야 하고, 점점 더 복잡해집니다.
이런 문제들을 해결하기 위해, 각 명령어 로직을 별도의 파일로 분리하고, 이를 체계적으로 불러와 사용하는 구조를 만드는 것이 중요합니다. 우리가 참고하고 있는 discordjs_typescript_boilerplate
가 바로 이런 구조를 잘 보여주고 있습니다.
명령어 파일의 기본 뼈대: commands
폴더와 파일 구조
우리 프로젝트의 src
폴더 안에 commands
라는 새 폴더를 만들어줍시다. 앞으로 모든 명령어 관련 파일들은 이 폴더 안에 차곡차곡 정리할 거예요. 이미 discordjs_typescript_boilerplate
를 사용하고 있다면 이 폴더가 존재할 겁니다.
# 터미널에서 프로젝트 루트 경로로 이동한 후 (src 폴더가 없다면 생성)
# mkdir src
cd src
mkdir commands # 이미 있다면 이 명령어는 생략
cd ..
각 명령어 파일은 TypeScript 파일(.ts
)로 만들고, 특정한 형식을 따르도록 할 겁니다. discordjs_typescript_boilerplate/src/commands/ping.ts
파일을 살펴보면 좋은 예시가 됩니다. 모든 명령어 파일은 기본적으로 다음 두 가지 요소를 내보내야(export) 합니다.
data
: 해당 명령어가 어떤 이름과 설명을 가졌는지, 어떤 옵션들을 받을 수 있는지 등을 정의하는 부분입니다. 슬래시 명령어에서는 SlashCommandBuilder
를 사용해서 이 부분을 구성합니다.execute
: 명령어가 실제로 실행될 때 호출될 함수입니다. 이 함수는 사용자의 입력(interaction
)을 받아 적절한 응답을 처리합니다.
예를 들어, ping
명령어를 위한 src/commands/ping.ts
파일은 다음과 같은 모습일 겁니다.
// src/commands/ping.ts
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
// 명령어의 기본 정보를 정의합니다.
export const data = new SlashCommandBuilder()
.setName("핑") // 슬래시 명령어 이름 (한글도 가능하지만, 보통 영어 소문자를 권장)
.setDescription("봇의 응답 속도를 확인합니다."); // 명령어에 대한 설명
// 명령어가 실행될 때 호출될 함수입니다.
export async function execute(interaction: ChatInputCommandInteraction) {
// interaction.reply()는 명령어에 대한 첫 응답을 보냅니다.
// ephemeral: true 옵션을 주면 명령어 사용자에게만 보이는 메시지를 보낼 수 있습니다.
const sentMessage = await interaction.reply({
content: "퐁! 응답 속도를 계산하고 있어요...",
fetchReply: true,
});
// fetchReply: true로 응답 메시지 객체를 받아온 후, editReply로 내용을 수정할 수 있습니다.
// 여기서는 실제 응답 속도를 계산해서 보여줍니다.
const latency = sentMessage.createdTimestamp - interaction.createdTimestamp;
await interaction.editReply(
`퐁! 🏓 응답 속도는 \\${latency}ms 입니다. API 지연 시간은 약 \\${Math.round(
interaction.client.ws.ping
)}ms 입니다.`
);
}
위 코드에서 setName
에 한글을 사용했지만, 디스코드의 정책이나 호환성을 위해 일반적으로 영어 소문자만 사용하는 것을 권장합니다. 여기서는 설명을 위해 한글 이름을 사용했습니다.
명령어들을 한곳에 모으기: src/commands/index.ts
개별 명령어 파일들을 만들었다면, 이제 이들을 한 곳에서 관리하고 쉽게 불러올 수 있도록 해야 합니다. discordjs_typescript_boilerplate
의 src/commands/index.ts
파일이 이 역할을 합니다.
이 파일은 commands
폴더 안에 있는 모든 개별 명령어 모듈들을 가져와서(import
) 하나의 객체로 묶어 내보냅니다(export
). 이렇게 하면 나중에 봇의 메인 파일(src/index.ts
)이나 명령어 배포 스크립트(src/deploy-commands.ts
)에서 모든 명령어에 쉽게 접근할 수 있습니다.
discordjs_typescript_boilerplate/src/commands/index.ts
는 다음과 같이 작성되어 있습니다.
// src/commands/index.ts
import * as ping from "./ping";
// 만약 다른 명령어가 있다면 여기에 추가합니다.
// import * as serverInfo from "./serverInfo";
export const commands = {
ping,
// serverInfo,
};
새로운 명령어를 추가할 때마다 이 파일에 해당 명령어를 import
하고 commands
객체에 추가해주면 됩니다.
메인 파일에서 명령어 처리하기: src/index.ts
이제 src/index.ts
파일에서 사용자의 상호작용(Interaction)이 발생했을 때, 우리가 정의한 명령어를 찾아 실행하도록 만들어야 합니다. 슬래시 명령어는 Events.InteractionCreate
이벤트를 통해 처리됩니다.
discordjs_typescript_boilerplate/src/index.ts
파일의 client.on(Events.InteractionCreate, ...)
부분을 보면 이 과정을 확인할 수 있습니다.
// src/index.ts (일부 발췌)
// ... (클라이언트 생성 및 다른 이벤트 핸들러) ...
client.on(Events.InteractionCreate, async (interaction) => {
try {
// 슬래시 커맨드인지 확인합니다.
if (!interaction.isChatInputCommand()) return;
// commands 객체에서 명령어 이름으로 해당 명령어 모듈을 가져옵니다.
const command = commands[interaction.commandName as keyof typeof commands];
// 명령어가 존재하지 않으면 아무것도 하지 않습니다.
if (!command) {
console.error(\`No command matching \\${interaction.commandName} was found.\\`);
return;
}
// 명령어의 execute 함수를 실행합니다.
await command.execute(interaction);
} catch (error) {
console.error('Error handling interaction:', error);
// 사용자에게 오류 메시지를 보낼 수도 있습니다.
if (interaction.isRepliable()) {
await interaction.reply({ content: '명령어 실행 중 오류가 발생했습니다.', ephemeral: true });
}
}
});
// ... (봇 로그인) ...
이 코드는 사용자가 슬래시 명령어를 입력하면, 해당 명령어 이름과 일치하는 모듈을 commands
객체에서 찾아 execute
함수를 실행합니다. 오류가 발생하면 콘솔에 기록하고, 사용자에게 간단한 오류 메시지를 보낼 수도 있습니다.
슬래시 명령어 등록: src/deploy-commands.ts
이렇게 명령어 파일을 만들고 index.ts
에 연결했다고 해서 바로 디스코드에서 슬래시 명령어를 사용할 수 있는 것은 아닙니다. 슬래시 명령어는 우리가 만든 명령어 정보를 디스코드 서버에 "우리 봇은 이런 명령어들을 가지고 있어요!"라고 등록해주는 과정이 필요합니다.
discordjs_typescript_boilerplate
프로젝트의 src/deploy-commands.ts
파일이 바로 이 역할을 합니다. 이 스크립트는 src/commands/index.ts
에 정의된 모든 명령어들의 data
부분을 읽어서 디스코드 API를 통해 특정 서버 또는 모든 서버에 명령어들을 등록합니다.
deploy-commands.ts
의 핵심 로직은 다음과 같습니다.
// src/deploy-commands.ts (일부 발췌)
import { REST, Routes } from "discord.js";
import { config } from "./config"; // DISCORD_TOKEN, DISCORD_CLIENT_ID 등이 담긴 설정 파일
import { commands } from "./commands"; // 우리가 정의한 명령어 모듈들
// 등록할 명령어들의 data 속성만 추출합니다.
const commandsData = Object.values(commands).map((command) => command.data.toJSON());
const rest = new REST({ version: "10" }).setToken(config.DISCORD_TOKEN);
export async function deployCommands({ guildId }: { guildId: string }) {
try {
console.log(\`Started refreshing application (/) commands for guild: \\${guildId}\\`);
// 특정 서버에 명령어를 등록합니다.
// 모든 서버에 등록하려면 Routes.applicationCommands(config.DISCORD_CLIENT_ID)를 사용합니다.
await rest.put(
Routes.applicationGuildCommands(config.DISCORD_CLIENT_ID, guildId),
{
body: commandsData,
}
);
console.log(\`Successfully reloaded application (/) commands for guild: \\${guildId}\\`);
} catch (error) {
console.error(error);
}
}
// boilerplate의 index.ts에서는 봇이 준비될 때 각 서버에 명령어를 배포합니다.
// 별도로 실행하고 싶다면 아래와 같이 특정 guildId를 지정하여 호출할 수 있습니다.
// deployCommands({ guildId: 'YOUR_TEST_SERVER_ID' });
discordjs_typescript_boilerplate
의 src/index.ts
에서는 봇이 준비될 때(`Events.ClientReady`) 연결된 모든 서버에 대해 이 deployCommands
함수를 호출하여 명령어를 자동으로 갱신합니다. 개발 중에는 이 방식이 편리할 수 있습니다.
만약 명령어를 수동으로, 또는 특정 서버에만 배포하고 싶다면 package.json
에 다음과 같이 스크립트를 추가할 수 있습니다.
// package.json
{
// ... 다른 내용 ...
"scripts": {
"start": "ts-node src/index.ts",
"dev": "ts-node-dev --respawn src/index.ts",
"deploy:guild": "ts-node src/deploy-commands-script.ts" // 예시 스크립트 이름
}
}
그리고 src/deploy-commands-script.ts
(또는 원하는 이름으로) 파일을 만들어 특정 서버 ID를 하드코딩하거나 환경 변수에서 읽어와 deployCommands
를 호출하도록 구성할 수 있습니다. 하지만 boilerplate의 자동 배포 방식이 대부분의 경우에 더 편리합니다.
실행하고 테스트하기
src/commands/ping.ts
파일이 위 예시처럼 작성되었는지 확인합니다.src/commands/index.ts
파일에ping
명령어가 올바르게 포함되었는지 확인합니다.src/config.ts
파일에DISCORD_TOKEN
과DISCORD_CLIENT_ID
가 정확히 설정되어 있는지 확인합니다.봇을 실행합니다.
npm run dev # 또는 npm start
봇이 실행되고 콘솔에 "Successfully reloaded application (/) commands."와 유사한 메시지가 뜨면, 디스코드 서버에서 /
를 입력해보세요. 우리가 만든 핑
명령어가 목록에 나타나고, 실행했을 때 응답 속도 메시지가 잘 나오는지 확인합니다.
마무리하며
오늘은 앞으로 만들어갈 수많은 명령어들을 담을 그릇, 즉 명령어 구조를 만드는 방법에 대해 알아봤습니다. 각 명령어를 별도의 파일로 분리하고, SlashCommandBuilder
를 사용해 명령어의 정보를 정의하며, 이를 commands/index.ts
를 통해 모으고, deploy-commands.ts
를 통해 디스코드에 등록하는 전체적인 흐름을 살펴보았습니다.
아직은 핑
명령어 하나뿐이지만, 이 구조 덕분에 앞으로 새로운 명령어를 추가하는 작업이 훨씬 수월해질 겁니다. 다음 시간에는 드디어 슬래시 명령어의 세계로 본격적으로 뛰어들어 보겠습니다. 슬래시 명령어에 옵션을 추가하는 방법, 서브 커맨드를 만드는 방법 등 더 다양하고 강력한 기능을 사용하는 방법을 자세히 파헤쳐 볼 예정입니다. 기대하셔도 좋습니다!
'DiscordJS 개발 튜토리얼' 카테고리의 다른 글
[DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리 (0) | 2025.06.05 |
---|---|
[DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게! (1) | 2025.06.04 |
[DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기 (2) | 2025.06.02 |
[DiscordJS 봇 개발 튜토리얼] 0. 프로젝트, 봇 생성하기 (3) | 2025.06.01 |