티미는 코드를 직접 실행시켜야지만 온라인 상태가 되네요.
온라인 상태일 때는 명령을 성공적으로 수행합니다.

티미를 만들고 작동을 시켜보니 정상적으로 돌아가기는 합니다. 다만, 티미가 돌아가도록 코드를 실행시키지 않은 상태에서 티미는 계속 오프라인이고 명령어를 실행하지도 않는다는 문제점이 있습니다.

코드 실행을 중지시키면 오프라인으로 전환되고 명령도 듣지 않습니다. ㅠㅠ

그렇다면 티미가 저 없이도 24시간 살아숨쉬게 하려면 어떻게 해야 할까요? 바로 AWS나 Heroku 등의 서비스를 통해서 호스팅을 하면 된다고 합니다. ㅎㅎ

 

호스팅을 하기 전에 코드를 먼저 완성해 놓아야 하겠죠!? 오늘은 저번 시간에 이어 Ukong0324님의 튜토리얼을 따라 차근차근 실습을 해보도록 하겠습니다. 이번 편은 커맨드 핸들링에 관한 내용이라고 하네요.

 

준비

  • 코드에서 사용될 fs 모듈이란?
    •  fs 모듈이란 File System의 약자로, Node.js의 여러 내장 모듈 중 하나라고 합니다. 파일을 읽거나 저장하는 등 파일 처리와 관련된 모듈이라고 하네요.
  • Commands 디렉토리 생성
    •  아래와 같이 루트 디렉토리에 Commands 디렉토리 및 그 하위 디렉토리, 파일을 생성했습니다.
    •  각 파일에는 저번 시간에 만들었던 명령어 코드들을 분리하여 넣어줄 예정입니다.

  • 아래와 같이 config.json 파일 생성하고 .gitignore에 추가하기 (외부에 노출되면 안되니까요!)
{
  "token": "봇 토큰 입력"
}

 

둘째 날

  • 전날 만들었던 코드를 아래와 같은 형식으로 각각의 파일에 분리하여 작성해주세요~
// Commands/Moderator/clean.js
exports.run = async (client, msg, args, prefix) => {
  // 이 안에는 어제 작성했던 명령어 코드를 각각 옮겨적어 주세요~
  // 여기선 clean 명령어 코드를 작성하면 되겠죠!?
  if (!args[0]) return msg.reply('청소할 만큼의 값을 정수로 적어주세요.');
    if (!Number(args[0])) return msg.reply('메시지를 지울 값으로는 반드시 숫자를 전달해주세요.');
    // Number는 인자로 전달한 값이 숫자로 암묵적 변환할 수 없는 값일 때 NaN을 반환합니다.
    if (args[0] < 1) return msg.reply('메시지를 지울 값은 1보다 커야 합니다.');
    if (args[0] > 100) return msg.reply('메시지를 지울 값은 100보다 작아야 합니다.');

    msg.channel.bulkDelete(args[0])
      .then(msg.reply(`${args[0]}만큼의 메시지를 성공적으로 삭제했습니다.`))
      .catch(console.error);
}

exports.config = {
  name: '청소', // 위 코드를 실행할 명령어 지정
  aliases: ['clear', 'clean'], // 명령어의 별명을 지정(이 단어들을 호출해도 위 코드가 실행됨.)
  category: ['moderator'], // 명령어 카테고리 지정
  des: ['bulkdelete'], // 명령어에 대한 설명
  use: ['!clear/clean <청소 할 메세지의 수>'] // 명령어 사용 방법 기재
}
  • 잠깐! 이런 분리 작업이 왜 필요한 걸까요?
    • 어제 우리가 작성했던 코드는 한 파일, 그것도 하나의 이벤트 핸들러 내에서 수많은 if문을 가지고 명령어에 따른 처리를 작성했습니다. 나무위키의 discord.js에 대한 설명에 따르면, 이렇게 if문을 늘려가다 보면 일명 스파게티 코드(프로그램 흐름이 복잡하게 뒤엉킨 모습을 스파게티에 비유한 표현_참고)를 발생시킬 수 있고 유지보수가 어렵기 때문에 좋지 않다고 해요. 대부분의 유명한 디스코드 봇들은 명령어/이벤트 파일들이 분리되어 있다고 하니 우리도 그런 식으로 작성을 해야겠네요.
  • 어제 작성했던 bot.js 파일을 튜토리얼 코드를 참고해서 수정합니다.
const Discord = require('discord.js');
const client = new Discord.Client();
const fs = require('fs'); // 커맨드 핸들러를 만들기 위해 fs 모듈 사용
const config = require('./config.json');

client.on('ready', () => {
  console.log(`${client.user.tag} 봇에 로그인했습니다.`);
});

/**
 * Collection은 discord.js의 유틸리티 클래스로,
 * 자바스크립트의 Map 클래스를 확장한 것이라고 합니다.
 */
client.commands = new Discord.Collection();
// 명령어 캐시 컬렉션을 클라이언트 내에 선언합니다.
client.aliases = new Discord.Collection();

/**
 * fs 모듈을 이용하여 ./Commands/ 폴더 안에 있는 내용을 불러와서 작업하기
 * readdir = read the contents of a directory
 * sync = Synchronous API → The synchronous APIs perform all operations "synchronously",
 * blocking the event loop until the operation completes or fails.
 * (즉, 이름 그대로 작업을 동기적으로 처리함.)
 * (참고: https://nodejs.org/api/fs.html#fs_fs_readdirsync_path_options)
 */
fs.readdirSync('./Commands/').forEach(dir => {
  // Filter라는 변수를 선언하고, Commands 폴더 내의 .js로 끝나는 파일들만 필터링한 배열을 할당합니다.
  const Filter = fs.readdirSync(`./Commands/${dir}`).filter(f => f.endsWith('.js'));
  /**
   * String.prototype.endsWith()
   * 어떤 문자열이 특정 문자열로 끝나는지를 확인한 뒤 boolean 값을 반환합니다.
   */

  console.log(Filter);
  /**
   * [ 'embed.js', 'pong.js', 'webhook.js' ]
   * [ 'ban.js', 'clean.js', 'kick.js' ]
   */

  Filter.forEach(file => {
    const cmd = require(`./Commands/${dir}/${file}`);

    client.commands.set(cmd.config.name, cmd);
    /**
     * set()은 Map 객체의 프로토타입 메서드로서,
     * 첫 번째 인자로는 추가/변경할 요소의 key를 전달하고
     * 두 번째 인자로는 해당 key의 value가 될 것을 전달합니다.
     */

    for (let alias of cmd.config.aliases) {
      // for...of문은 순회 가능한 자료 구조(이터러블)를 순회합니다.
      client.aliases.set(alias, cmd.config.name);
    }
  });
});

// 명령어를 실행할 때 사용할 함수
function runCommand(command, message, args, prefix) {
  const cmd = client.commands.get(command)
    ? client.commands.get(command)
    : client.commands.get(client.aliases.get(command));
    /**
     * get()은 Map 객체의 프로토타입 메서드로서,
     * 인자로 전달한 key에 해당하는 value를 반환합니다.
     * (해당하는 값이 없을 경우 undefined 반환)
     */

    if (cmd) cmd.run(client, message, args, prefix);
    /**
     * 만약 입력한 값에 대응하는 명령어가 존재한다면, 해당 명령어를 실행시킵니다.
     * run() → This methods runs a function synchronously within a context
     * and return its return value.
     */
    return;
}

client.on('message', async msg => {
  const prefix = '!';

  if (!msg.content.startsWith(prefix)) return;
  /**
   * String.prototype.startsWith()
   * 어떤 문자열이 특정 문자로 시작하는지를 확인한 뒤 boolean 값을 반환합니다.
   */
  
  let args = msg.content.slice(prefix.length).trim().split(/ +/g);
  let command = args.shift().toLowerCase();

  try {
    runCommand(command, msg, args, prefix);
  } catch (err) { // 명령어 실행 도중 에러가 발생할 경우 에러 메시지 출력
    console.error(err);
  }
});

client.login(config.token);
// config.json 파일에 저장해 둔 token 값 불러오기
  • 그럼 이제 코드를 실행 시키고 티미를 테스트해볼까요!?

잘 되네요!

  • 명령어를 직접 입력해줘도 되고, 별명으로 지정한 단어를 입력해줘도 명령을 수행하려는 것을 확인할 수 있습니다!

 

다음 시간에는 도움말 명령어를 작성해보고, 팀짜기 로직을 적용시키는 것까지 해보겠습니다!

복사했습니다!