[DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!

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

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

지난 시간에는 명령어들을 체계적으로 관리하기 위한 폴더 구조를 만들고, 첫 번째 슬래시 명령어인 /핑을 등록해보았습니다. 이제 우리 봇은 단순한 텍스트 기반 명령어가 아닌, 디스코드 인터페이스에 깔끔하게 통합되는 슬래시 명령어를 사용할 준비가 되었죠.

이번 시간에는 여기서 한 걸음 더 나아가, 슬래시 명령어를 더욱 강력하고 유용하게 만들어주는 옵션(Options)과 서브커맨드(Subcommands)에 대해 자세히 알아보겠습니다. 사용자와 더 다양한 방식으로 상호작용하고, 복잡한 기능도 깔끔하게 구현할 수 있게 될 거예요.

슬래시 명령어에 날개를 달아주는 '옵션'

지금까지 만든 /핑 명령어는 사용자가 단순히 /핑이라고 입력하면 정해진 응답을 보내는 방식이었습니다. 하지만 많은 경우, 명령어에 추가적인 정보를 함께 전달하고 싶을 때가 있습니다. 예를 들어, "/인사 [이름]"처럼 특정 사용자에게 인사하거나, "/검색 [키워드]"처럼 특정 내용을 검색하는 명령어를 만들고 싶을 수 있죠. 이럴 때 사용하는 것이 바로 '옵션'입니다.

SlashCommandBuilder는 다양한 종류의 옵션을 추가할 수 있는 메서드들을 제공합니다.

  • addStringOption(): 문자열 입력
  • addIntegerOption(): 정수 입력
  • addNumberOption(): 숫자 (정수 또는 실수) 입력
  • addBooleanOption(): 참/거짓 선택
  • addUserOption(): 사용자 멘션
  • addChannelOption(): 채널 선택
  • addRoleOption(): 역할 선택
  • addMentionableOption(): 사용자 또는 역할 멘션
  • addAttachmentOption(): 파일 첨부

각 옵션 메서드는 체이닝(chaining) 방식으로 설정할 수 있으며, 공통적으로 다음과 같은 세부 설정을 할 수 있습니다.

  • setName(name): 옵션의 이름 (코드에서 이 이름으로 값을 가져옴, 영어 소문자 권장)
  • setDescription(description): 옵션에 대한 설명 (사용자에게 표시됨)
  • setRequired(boolean): 이 옵션이 필수인지 아닌지 설정 (기본값은 false)

예제: 사용자에게 인사하는 명령어 만들기

그럼 /인사라는 명령어를 만들고, user 타입의 옵션과 message 타입의 옵션을 추가해보겠습니다. user 옵션은 필수로, message 옵션은 선택으로 설정해볼게요.

src/commands/인사.ts (또는 greet.ts) 파일을 새로 만들고 아래와 같이 작성합니다.

// src/commands/인사.ts
import { SlashCommandBuilder, ChatInputCommandInteraction, User } from 'discord.js';

export const data = new SlashCommandBuilder()
    .setName('인사')
    .setDescription('지정한 사용자에게 인사 메시지를 보냅니다.')
    .addUserOption(option =>
        option.setName('대상') // 옵션 이름
            .setDescription('인사할 대상을 선택하세요.') // 옵션 설명
            .setRequired(true) // 필수 옵션으로 설정
    )
    .addStringOption(option =>
        option.setName('메시지')
            .setDescription('함께 전달할 메시지 (선택 사항)')
            .setRequired(false) // 선택적 옵션
    );

export async function execute(interaction: ChatInputCommandInteraction) {
    // 옵션 값 가져오기
    const targetUser = interaction.options.getUser('대상'); // '대상' 옵션에서 User 객체를 가져옴
    const customMessage = interaction.options.getString('메시지'); // '메시지' 옵션에서 문자열을 가져옴

    if (!targetUser) {
        // 혹시 모를 경우에 대비 (setRequired(true)로 설정했으므로 보통은 null이 아님)
        return interaction.reply({ content: '인사할 대상을 찾을 수 없어요.', ephemeral: true });
    }

    let replyMessage = \`\\${targetUser.toString()}님, 안녕하세요! 👋\\\`;

    if (customMessage) {
        replyMessage += `\\n> \\${customMessage}\`;
    }

    await interaction.reply(replyMessage);
}

코드 설명:

  • addUserOption(option => ...): 대상이라는 이름의 사용자 옵션을 추가합니다. setRequired(true)로 필수 입력하도록 했습니다.
  • addStringOption(option => ...): 메시지라는 이름의 문자열 옵션을 추가하고, 선택 사항으로 남겨둡니다.
  • interaction.options.getUser('대상'): execute 함수 내에서 interaction.options 객체의 getUser() 메서드를 사용해 '대상' 옵션으로 전달된 User 객체를 가져옵니다. 다른 타입의 옵션도 getString(), getInteger(), getBoolean() 등으로 가져올 수 있습니다.
  • customMessage가 있다면 인사 메시지에 추가합니다.

이 파일을 만들었다면, src/commands/index.ts에 인사 명령어를 추가하고, npm run dev (또는 npm start)로 봇을 재시작하여 디스코드에 명령어가 잘 등록되는지 확인합니다. (boilerplate는 봇 시작 시 자동으로 명령어를 배포합니다.)

이제 디스코드에서 /인사를 입력하면 대상을 선택하는 필드가 나타나고, 선택적으로 메시지도 입력할 수 있게 됩니다.

옵션에 선택지(Choices) 추가하기

때로는 사용자가 특정 값들 중에서만 선택하도록 하고 싶을 수 있습니다. 이럴 때 addChoices() 메서드를 사용합니다.

예를 들어, /음식추천이라는 명령어를 만들고, 음식 종류를 '한식', '중식', '일식', '양식' 중에서 고르도록 해봅시다.

// src/commands/음식추천.ts
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';

export const data = new SlashCommandBuilder()
    .setName('음식추천')
    .setDescription('선택한 종류의 음식을 추천해줍니다.')
    .addStringOption(option =>
        option.setName('종류')
            .setDescription('원하는 음식 종류를 선택하세요.')
            .setRequired(true)
            .addChoices(
                { name: '🍚 한식', value: 'korean' },
                { name: '🍜 중식', value: 'chinese' },
                { name: '🍣 일식', value: 'japanese' },
                { name: '🍕 양식', value: 'western' }
            )
    );

export async function execute(interaction: ChatInputCommandInteraction) {
    const foodType = interaction.options.getString('종류');
    let recommendation = '';

    switch (foodType) {
        case 'korean':
            recommendation = '오늘은 뜨끈한 국밥 어떠세요?';
            break;
        case 'chinese':
            recommendation = '짜장면과 탕수육 조합은 진리죠!';
            break;
        case 'japanese':
            recommendation = '신선한 초밥이나 라멘이 좋겠네요.';
            break;
        case 'western':
            recommendation = '피자나 파스타로 든든하게 채워보세요!';
            break;
        default:
            recommendation = '음... 뭘 먹어야 할까요?';
    }

    await interaction.reply(\`오늘의 추천 메뉴: \\${recommendation}\`);
}

addChoices()에는 { name: '사용자에게 보여질 이름', value: '코드에서 사용할 값' } 형태의 객체 배열을 전달합니다. 사용자가 '🍚 한식'을 선택하면, 코드에서는 interaction.options.getString('종류')의 결과로 'korean' 값을 받게 됩니다.

하나의 명령어, 여러 기능: 서브커맨드와 서브커맨드 그룹

명령어의 기능이 점점 복잡해지면, 관련된 여러 기능을 하나의 상위 명령어 아래에 묶고 싶을 때가 있습니다. 예를 들어, /정보라는 명령어가 있고, 이 아래에 유저 정보 보기, 서버 정보 보기 같은 하위 기능들을 두고 싶을 수 있죠. 이럴 때 사용하는 것이 서브커맨드(Subcommand)와 서브커맨드 그룹(Subcommand Group)입니다.

  • 서브커맨드: /명령어 subcommand 형태로 사용됩니다. (예: /정보 유저)
  • 서브커맨드 그룹: /명령어 group subcommand 형태로, 서브커맨드를 한 단계 더 그룹화할 수 있습니다. (예: /설정 메시지 알림)

서브커맨드 만들기

SlashCommandBuilder에서 addSubcommand() 메서드를 사용하여 서브커맨드를 정의할 수 있습니다. 각 서브커맨드는 자신만의 이름, 설명, 그리고 옵션을 가질 수 있습니다.

예제: /정보 명령어에 유저와 서버 서브커맨드 만들기

// src/commands/정보.ts
import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember, Guild } from 'discord.js';

export const data = new SlashCommandBuilder()
    .setName('정보')
    .setDescription('사용자 또는 서버의 정보를 보여줍니다.')
    .addSubcommand(subcommand =>
        subcommand
            .setName('유저')
            .setDescription('선택한 사용자의 정보를 보여줍니다.')
            .addUserOption(option => option.setName('대상').setDescription('정보를 볼 사용자').setRequired(true))
    )
    .addSubcommand(subcommand =>
        subcommand
            .setName('서버')
            .setDescription('현재 서버의 정보를 보여줍니다.')
    );

export async function execute(interaction: ChatInputCommandInteraction) {
    const subcommand = interaction.options.getSubcommand(); // 사용된 서브커맨드의 이름을 가져옵니다.

    if (subcommand === '유저') {
        const user = interaction.options.getUser('대상');
        if (!user) return interaction.reply({ content: '사용자를 찾을 수 없습니다.', ephemeral: true });

        // 서버 멤버 정보를 가져오기 위해 interaction.member를 사용하거나, fetchMember를 고려할 수 있습니다.
        const member = interaction.guild?.members.cache.get(user.id) || await interaction.guild?.members.fetch(user.id);

        await interaction.reply(
            `사용자 이름: \\${user.username}\\n` +
            `ID: \\${user.id}\\n` +
            (member instanceof GuildMember ? \`서버 합류일: \\${member.joinedAt?.toLocaleDateString()}\\n\` : '') +
            `계정 생성일: \\${user.createdAt.toLocaleDateString()}`
        );
    } else if (subcommand === '서버') {
        const guild = interaction.guild;
        if (!guild) return interaction.reply({ content: '서버 정보를 가져올 수 없습니다. DM에서는 사용할 수 없어요.', ephemeral: true });

        await interaction.reply(
            `서버 이름: \\${guild.name}\\n` +
            `총 멤버 수: \\${guild.memberCount}\\n` +
            `서버 생성일: \\${guild.createdAt.toLocaleDateString()}\\n` +
            `서버 주인: \\${(await guild.fetchOwner()).user.tag}`
        );
    }
}

코드 설명:

  • .addSubcommand(subcommand => ...): 각 서브커맨드를 정의합니다. 유저 서브커맨드는 대상이라는 사용자 옵션을 추가로 받습니다.
  • interaction.options.getSubcommand(): execute 함수 내에서 어떤 서브커맨드가 사용되었는지 그 이름을 문자열로 가져옵니다.
  • 이후 if/else if 문을 사용하여 각 서브커맨드에 맞는 로직을 실행합니다.

서브커맨드 그룹 만들기 (간단 소개)

서브커맨드 그룹은 addSubcommandGroup() 메서드를 사용합니다. 그룹 안에 다시 addSubcommand()를 사용하여 실제 서브커맨드들을 정의할 수 있습니다.

// 예시: /config group subcommand
new SlashCommandBuilder()
  .setName("config")
  .setDescription("봇 설정을 관리합니다.")
  .addSubcommandGroup((group) =>
    group
      .setName("prefix")
      .setDescription("접두사 관련 설정을 관리합니다.")
      .addSubcommand((subcommand) =>
        subcommand
          .setName("set")
          .setDescription("새로운 접두사를 설정합니다.")
          .addStringOption((option) =>
            option
              .setName("value")
              .setDescription("새 접두사")
              .setRequired(true)
          )
      )
      .addSubcommand((subcommand) =>
        subcommand
          .setName("view")
          .setDescription("현재 설정된 접두사를 봅니다.")
      )
  );
// execute 함수에서는 interaction.options.getSubcommandGroup() 와 interaction.options.getSubcommand() 를 함께 사용합니다.

서브커맨드 그룹은 명령어가 매우 복잡해질 때 유용하지만, 너무 많은 단계를 거치면 사용자 경험이 나빠질 수 있으니 신중하게 사용하는 것이 좋습니다.

명령어 배포의 중요성: deploy-commands.ts 다시 보기

지난 시간에 잠깐 언급했듯이, 슬래시 명령어(옵션이나 서브커맨드를 포함하여)를 만들거나 수정했다면, 변경된 내용을 디스코드에 알려주어야 합니다. 이 역할을 하는 것이 src/deploy-commands.ts 스크립트입니다.

discordjs_typescript_boilerplate에서는 봇이 시작될 때(Events.ClientReady) 연결된 모든 서버에 대해 자동으로 명령어를 배포(갱신)합니다. 이는 개발 중에는 매우 편리합니다.

deploy-commands.ts는 src/commands/index.ts에서 가져온 모든 명령어들의 data 속성 (즉, SlashCommandBuilder로 정의된 명령어 구조)을 JSON 형태로 변환하여 디스코드 API로 전송합니다. 디스코드는 이 정보를 바탕으로 사용자 인터페이스에 슬래시 명령어를 표시하고, 옵션 입력 필드를 제공하며, 서브커맨드 선택지를 보여줍니다.

명령어 업데이트 시 주의사항:

  • 명령어의 이름, 설명, 옵션, 서브커맨드 구조 등을 변경했다면, 봇을 재시작하여 deploy-commands 로직이 다시 실행되도록 해야 합니다.
  • 때로는 디스코드 클라이언트 자체에 명령어 정보가 캐시되어 즉시 반영되지 않는 것처럼 보일 수 있습니다. 이 경우, 몇 분 정도 기다리거나, 디스코드 클라이언트를 완전히 재시작(Ctrl/Cmd + R)해보는 것이 좋습니다.
  • 글로벌 명령어 vs 서버(Guild) 명령어: deploy-commands.ts에서 Routes.applicationGuildCommands(clientId, guildId)를 사용하면 특정 서버에만 명령어를 등록합니다. 개발 및 테스트 시에는 이 방식이 빠르고 편리합니다. 모든 서버에 명령어를 등록하려면 Routes.applicationCommands(clientId)를 사용하며, 이를 '글로벌 명령어'라고 합니다. 글로벌 명령어는 모든 서버에 적용되지만, 전파되는 데 최대 1시간까지 걸릴 수 있습니다. Boilerplate는 각 서버에 개별적으로 등록하는 방식을 사용하고 있습니다.

마무리

이번 시간에는 슬래시 명령어에 옵션을 추가하여 사용자로부터 다양한 입력을 받고, 서브커맨드를 사용하여 관련된 기능들을 하나의 명령어로 묶는 방법을 배웠습니다. 이를 통해 훨씬 더 유연하고 강력한 명령어를 설계할 수 있게 되었습니다.

다음 시간에는 명령어 사용에 제한을 두는 '쿨타임' 기능과, 명령어 실행 중 발생할 수 있는 다양한 '오류'들을 어떻게 효과적으로 처리하고 사용자에게 피드백을 줄 수 있는지에 대해 알아보겠습니다. 안정적인 봇 운영을 위한 필수적인 내용이니 기대해주세요!

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

[DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리  (0) 2025.06.05
[DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음  (1) 2025.06.03
[DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기  (2) 2025.06.02
[DiscordJS 봇 개발 튜토리얼] 0. 프로젝트, 봇 생성하기  (3) 2025.06.01
'DiscordJS 개발 튜토리얼' 카테고리의 다른 글
  • [DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리
  • [DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음
  • [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 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!
상단으로

티스토리툴바