오늘은 튜토리얼을 참고하여 명령어에 대한 사용 방법 등을 조회할 수 있는 도움말 명령어를 작성해보도록 하겠습니다.

 

준비

  • 전날 작성했던 새로운 bot.js 파일에서 아래 코드의 마지막 줄과 같이, client에 category라는 프로퍼티를 새롭게 추가해주세요!
  • 곧 작성할 도움말 명령어에서 카테고리들을 쉽게 불러올 수 있도록 하기 위함이라고 하네요.
// bot.js
client.commands = new Discord.Collection();
client.aliases = new Discord.Collection();
client.category = ['bot', 'moderator'];

셋째 날

  • 이번 시간에는 !도움말 + 명령어(예를 들어 '!도움말 청소')를 입력하면 해당 명령어에 대한 사용 방법과 그 명령어의 별명(해당 명령을 실행시킬 수 있는 다른 단어)을 안내하는 임베드(embed)가 뜨도록 도움말 명령어를 작성해보겠습니다!
    • 디스코드에서 임베드는 아래와 같이 나타납니다~

이미지 출처: https://stackoverflow.com/questions/55324281/embed-not-showing-correctly

  • 전날 생성한 Commands/Bot 디렉토리에 help.js 파일을 생성한 뒤 아래와 같은 코드를 작성했어요.
// Commands/Bot/help.js
const Discord = require('discord.js');

exports.run = async (client, msg, args, prefix) => {
  // 🎀 '!도움말' + 명령어를 입력한 경우
  if (args[0]) {
    if (!client.commands.get(args[0]) && !client.aliases.get(args[0])) {
      return msg.reply(`${args[0]}에 대한 정보를 찾을 수 없습니다.`);
    }
  
    const command = client.commands.get(args[0])
      ? client.commands.get(args[0]) // 명령어를 입력한 경우
      : client.commands.get(client.aliases.get(args[0])); // 명령어의 별명을 입력한 경우
  
    const config = command.config;
    const name = config.name;
    const aliases = config.aliases;
    const category = config.category;
    const description = config.des;
    const use = config.use;
  
    const Command = new Discord.MessageEmbed()
      .setTitle(`${name} 명령어`)
      .setColor('#0ea085')
      .setDescription(`\`\`\`fix\n사용법: ${use}\`\`\``)
      .addField('명령어 설명', `**${description}**`, false)
      .addField('카테고리', `**${category}**`, true)
      .addField('명령어의 별명', `**${aliases}**`, true);
  
    msg.reply(Command);
    return;
  }

  // 🎀 '!도움말'만 입력한 경우
  const categorys = client.category;
  // bot.js의 client.category를 categorys로 선언했습니다.

  const Commands = new Discord.MessageEmbed() // 메시지에 embed를 나타낸다고 합니다.
    .setAuthor(client.user.username + '봇 명령어', client.user.displayAvatarURL())
    .setColor('#0ea085')
    .setFooter(`${prefix}도움 <명령어>를 입력하여 해당 명령어를 자세히 확인해보세요.`);

  for (const category of categorys) {
    Commands.addField(
      category,
      `> **\`${client.commands.filter(el => el.config.category[0] === category).keyArray().join('`, `')}\`**`
    );
    /**
     * addField는 embed에서 소제목과 소 설명을 설정하는데요,
     * 첫 번째 인자로는 소제목으로 설정할 값을 전달해주고
     * 두 번째 인자로는 소 설명으로 설정할 값을 전달해줍니다.
     * 세 번째 인자로는 inline 요소처럼 보여질지에 대한 boolean 값을 전달합니다.
     * (false를 전달할 경우 block 요소처럼 보여집니다. 즉, 한칸을 다 차지합니다.)
     * 
     * keyArray는 Collection을 배열로 바꾸는 방법 중 하나라고 합니다.
     */
  }

  msg.reply(Commands);
};

exports.config = {
  name: '도움말',
  aliases: ['도움', '명령어', 'commands', 'help'],
  category: ['bot'],
  des: ['봇에 대한 명령어 리스트들을 불러와드립니다.'],
  use: ['!도움말 <명령어>']
};
  • 코드를 하나씩 살펴보겠습니다~
  • 우선 아래 그림을 보시면, !도움말 + 명령어라고 입력할 경우에는 왼쪽 그림과 같은 설명이 뜨도록 할 것이고, !도움말이라고만 입력할 경우에는 오른쪽 그림과 같은 설명이 뜨도록 하기 위해서 if문으로 한 번 분기를 해줬습니다. (명령어를 같이 입력한다면 args[0]은 해당 명령어를 가리키게 됩니다. 명령어가 없을 경우에는 undefined가 반환되어요.)

 

 

  •  도움말을 보려는 명령어를 함께 입력한 경우라도, 티미의 명령어 목록에 없는 명령어(또는 명령어를 불러올 수 있는 별명)라면 해당 명령어에 대한 정보가 없음을 알려줍니다.

 

  • 그리고 명령어(예: 임베드)를 입력했는지, 해당 명령어의 별명(예: dlaqpem)을 입력했는지에 따라 command 변수에 할당할 값을 달리 정해줍니다.

  • client.commands는 bot.js에서 가져온 Map 객체인데요, 지난 시간에 fs 모듈을 사용해서 Map 객체인 client.commands와 client.aliases에 요소들을 추가해줬었죠?

  • 이렇게 요소를 추가해놓은 client.commands를 살펴보면 아래 코드와 같아요.
// client.commands를 console.log로 출력해보면,
Collection(8) [Map] {
  '임베드' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '임베드',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  // 각 key에 대한 value에, 해당 명령어에 해당하는 모듈들을 할당했으므로
  // run과 config의 구체적인 값은 아래 주석과 같습니다.
  /**
  run: async (client, msg, args, prefix) => {
         const embed = new Discord.MessageEmbed()
          .setTitle('여기는 대표 타이틀!')
          .setDescription('여기는 대표 설명!')
          .setColor('DARK_GOLD')
          .setFooter('여기는 푸터!')
          .setThumbnail('xxx.jpg')
          .setImage('xxx.png')
          .setTimestamp()
          .addField('여기는 소제목', '여기는 설명');
    
          msg.reply(embed);
       },
  config: {
    name: '임베드',
    aliases: ['embed', 'dlaqpem'],
    category: ['bot'],
    des: ['임베드에 대한 설명'],
    use: ['!임베드']
  }
  */
  '도움말' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '도움말',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  '핑' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '핑',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  '팀짜기' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '팀짜기',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  '웹훅' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '웹훅',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  '차단' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '차단',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  '청소' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '청소',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  },
  '추방' => {
    run: [AsyncFunction (anonymous)],
    config: {
      name: '추방',
      aliases: [Array],
      category: [Array],
      des: [Array],
      use: [Array]
    }
  }
}
  • 이제 여기에서 config가 가리키는 객체로부터 여러 값들을 이용해서 명령어 안내 임베드를 만들어줍니다. MessageEmbed 클래스를 사용해서 Command라는 인스턴스를 만들어줄 건데요, MessageEmbed의 다양한 메서드들을 메서드 체이닝으로 사용하며 Command를 완성시켜줍니다.

 

  • MessageEmbed는 다양한 메서드를 갖는데요, 위에서 사용한 메서드들이 각각 어떤 역할을 하고 임베드에 실제로 어떤 식으로 표시되는지 간단히 살펴보겠습니다. 
    • setTitle → 임베드의 제목을 나타내요.
    • setColor → 임베드 모서리 부분의 색깔을 지정해요.
    • setDescription → 임베드의 설명을 나타내요.
      • 디스코드가 일부 마크다운 문법을 지원해서 저런 식으로 인자에 코드 블록이나 볼드체를 설정해서 전달할 수도 있습니다.
      • 저도 튜토리얼을 보면서 작성하다 보니까 처음에 코드만 보면서 작성할 때는 아리송했는데, 다 작성하고 결과물을 보니 그저 늘 쓰던 마크다운 문법이더라구요. 인자로 마크다운 문법을 전달하면 그대로 적용이 되는 게 신기하네요. ㅎㅎ
        • ```언어
          코드
          ```
          이는 해당 언어에 맞게 하이라이팅된 코드 블록을 설정합니다. 위에서는 escape을 위해 각각의 백틱(`)에 escape character(백슬래쉬)를 함께 써줬습니다. 또한 위의 백틱 3개 다음에 한 줄 띄고 코드를 작성해줘야 하기 때문에 줄바꿈을 나타내는 이스케이프 시퀀스인 \n를 fix 다음에 써줬습니다.
        • 마크다운 문법에서 텍스트를 앞뒤로 각각 애스터리스크(*) 2개로 감싸면(예: **true**) 해당 텍스트가 볼드체로 표시됩니다.
        • 이따가 다른 코드를 설명드릴 때 나올 마크다운 문법들도 미리 설명드리고 넘어가자면, 백틱 하나로 감싼 텍스트(예: `true`)는 인라인 코드 블록으로 표시됩니다. 그리고 >를 사용하면 블록 인용을 나타낼 수 있는데요, 예시는 이따가 보여드리겠습니다.
    • addField → 임베드에 필드를 추가하는데요, 최대 25개까지 가능합니다. addField 하나 당 하나의 소제목(첫 번째 인자로 전달)과 하나의 소 설명(두 번째 인자로 전달)을 추가하고, 추가로 세 번째 인자에 boolean 값을 전달해서 임베드 내에서 인라인 요소처럼 보여지게 할 지 블록 요소처럼 보여지게 할지도 정할 수 있어요.

  • addField의 세 번째 인자를 true로 전달할 경우 해당 필드가 인라인으로 보여지고, false를 전달할 경우에는 블록으로 보여집니다. (아래 그림 참고)

'카테고리' 필드의 세 번째 인자에 true를 줄 때(위)와 false를 줄 때(아래) 비교

  • 그리고 이렇게 만든 Command를 답장으로서 전달하도록 msg.reply()의 인자로 Command를 전달해줍니다.

결과

 

  • 다음으로, !도움말만 입력한 경우의 코드를 보겠습니다.
  • 앞서 명령어와 함께 입력한 경우와 마찬가지로 Commands라는 MessageEmbed의 인스턴스를 생성해주고 역시 메서드 체이닝을 통해 임베드를 완성해줍니다. 위와 달리 setAuthor과 setFooter가 등장하네요.
    • setAuthor → 임베드 작성자를 표시해요.
      • 첫 번째 인자: 작성자 이름으로 표시될 문자열을 전달
      • 두 번째 인자(옵션): 작성자 아이콘으로 표시될 아이콘 URL을 문자열로 전달
      • 세 번째 인자(옵션): 작성자 정보를 나타내는 URL을 문자열로 전달
    • setFooter → 임베드의 푸터 내용을 나타내요.

setAuthor와 setFooter로 설정한 내용은 각각 이렇게 표시돼요.

  • 그리고 Commands 임베드에 필드를 추가해 줄 건데요, 각 명령어 카테고리에 어떤 명령어들이 존재하는지를 나타내기 위해 아래와 같이 코드를 작성했습니다. bot.js에서 지정해줬던 client.category라는 배열을 순회하면서 요소 하나 당 한 번씩 Commands에 필드를 추가합니다. 해당 요소(명령어 카테고리를 나타내는 문자열)와 카테고리 이름이 일치하는 명령어들만 뽑아서 필드 설명에 올리는 작업을 합니다.

뭔가 이상해요!

  • 여기서 잠깐! 뭔가 이상한 점이 있습니다. 아까 client.commands가 Map 객체라고 하지 않았나요?? 그런데 Map 객체가 어떻게 Array의 프로토타입 메서드인 filter 메서드를 사용하는데도 오류가 나지 않는 것일까요?? 일반 Map 객체에 filter 메서드를 사용할 경우 아래와 같이 에러가 발생하는데 말입니다...

ㅠㅠ

  • 그것은 바로, 여기서의 filter 메서드는 Array.prototype.filter가 아니라, discord.js의 Collection 클래스가 제공하는 'Array-like methods' 중 하나이기 때문입니다! 그리고 Collection 클래스의 인스턴스인 client.commands도 이 filter 메서드를 사용할 수 있는 것이구요. discord.js는 이처럼 Map 객체를 생성하는 Collection 클래스에 filter 외에도 이와 같은 'Array-like methods'를 지원해줌으로써, 사용자가 유용한 배열 메서드들을 사용하려는 목적으로 Map 객체를 매번 배열로 변경하는 번거로움을 줄여줍니다.
  • 이 filter 메서드는 당연히 배열이 아니라 Map 객체를 반환하는데요, 반환된 Map 객체의 key 값들만 뽑아서 배열로 만든 뒤 이를 문자열로 변환해주기 위해서 keyArray 메서드와 join 메서드를 순서대로 사용했습니다.
  • 그리고 이렇게 만들어진 Commands를 티미가 답장하도록 설정해줍니다. 그럼 결과를 볼까요?

결과

 

 

다음 시간에는 팀짜기 명령어를 작성해보도록 하겠습니다~~

 


참고 문서

discordjs.guide/additional-info/collections.html#array-like-methods

 

Discord.js Guide

A guide made by the community of discord.js for its users.

discordjs.guide

discord.js.org/#/docs/main/stable/class/MessageEmbed?scrollTo=addField

 

Discord.js

Discord.js is a powerful node.js module that allows you to interact with the Discord API very easily. It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.

discord.js.org

 

복사했습니다!