[DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음

2025. 6. 3. 17:10·DiscordJS 개발 튜토리얼

해당 글은, 제가 작성한 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의 자동 배포 방식이 대부분의 경우에 더 편리합니다.

실행하고 테스트하기

  1. src/commands/ping.ts 파일이 위 예시처럼 작성되었는지 확인합니다.

  2. src/commands/index.ts 파일에 ping 명령어가 올바르게 포함되었는지 확인합니다.

  3. src/config.ts 파일에 DISCORD_TOKEN과 DISCORD_CLIENT_ID가 정확히 설정되어 있는지 확인합니다.

  4. 봇을 실행합니다.

    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
'DiscordJS 개발 튜토리얼' 카테고리의 다른 글
  • [DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리
  • [DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!
  • [DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기
  • [DiscordJS 봇 개발 튜토리얼] 0. 프로젝트, 봇 생성하기
디스호스트
디스호스트
쉽고 안정적인 디스코드 봇 호스팅 서비스, 디스호스트의 기술 블로그입니다. 디스호스트는 24시간 구동되는 서버를 통해 디스코드 봇을 대신 구동시켜 드리는 서비스를 제공하고 있습니다.
  • 디스호스트
    디스호스트 기술 블로그
    디스호스트
  • 블로그 메뉴

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

    • 디스호스트
    • 디스호스트 패널
  • hELLO· Designed By정상우.v4.10.3
디스호스트
[DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음
상단으로

티스토리툴바