오늘은 랜덤 팀짜기 명령어를 작성해보도록 하겠습니다! 사실 매화봇이라는 유명하고 아주 유용한 봇도 이러한 팀짜기 기능을 제공하고 있으며 저도 매화봇을 사용해왔지만, 팀짜기 기능은 한번 직접 구현해보고 싶어서 봇을 만들게 되었습니다. ㅎㅎ

 

준비

  • bot.js와 명령어 코드들을 작성하기 전 랜덤 팀짜기 로직을 먼저 구현해놨었는데요, 팀짜기 명령어에 이 로직을 적용하는 것이 오늘의 목표입니다! 자바스크립트로 작성한 랜덤 팀짜기 로직은 아래와 같습니다.
// utils/teamMaker.js
/**
 * @param {array} members 전체 명단을 배열로 전달
 * @param {number} number 한 팀당 팀원 수를 숫자(정수)로 전달
 * @returns 팀 구성 결과를 배열로 반환
 */

module.exports = function makeTeam(members = [], number = 1) {

  // 각 멤버에게 임의의 key 값을 부여
  const generateRandomKey = () => Math.floor(Math.random() * 1000);

  const membersWithKey = members.map(member => ({
    name: member,
    key: generateRandomKey(),
  }));

  // 각 멤버를 key 값의 크기에 따라 오름차순으로 정렬
  const sortResult = membersWithKey.sort((a, b) => {
    return a.key >= b.key ? 1 : -1;
  });

  // 정렬 결과에서 임의로 부여했던 key 프로퍼티를 제거
  const membersWithoutKey = sortResult.map(res => res.name);

  // number parameter로 전달했던 팀원 수에 맞게 팀 나누기
  const teamResult = [];

  for (let i = 0; i < members.length; i++) {
    const piece = [...membersWithoutKey].slice(i, i + number);
    teamResult.push(piece);

    i += number - 1;
  }

  return teamResult;
}

/* -------------------------------- test case ------------------------------- */
// console.log(makeTeam(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], 3));
/**
[
  [ 'h', 'f', 'g' ], // 1팀
  [ 'a', 'd', 'e' ], // 2팀
  [ 'c', 'b' ] // 3팀
]
 */

 

<코드 설명>

- 코드의 가독성을 지키고 (언젠가) 재사용할 때를 대비해서 utils 폴더를 생성하여 여기에 로직을 따로 분리해뒀습니다. 이 makeTeam이라는 함수는 members라는 매개변수를 통해 음성 채널에 있는 멤버들의 목록을 배열 형태로 전달 받을 것이고, number라는 매개변수를 통해 각 팀당 인원 수를 나타내는 숫자 값을 전달 받을 것입니다.

 

- 이제 매개변수를 통해 전달 받은 재료들을 가지고 랜덤으로 팀을 짜고 싶은데 어떤 기준으로 섞어야 될 지 고민이 되었습니다. 함수를 돌릴 때마다 다른 결과가 나와야 하고, 팀 배정이 어떤 규칙을 갖고 있으면 안되고 무작위로 결과를 내야 했기 때문에, Math.random 메서드를 사용하기로 했습니다. 그래서 우선은 namekey라는 프로퍼티를 가진 객체를 요소로 갖는 배열을 만들었습니다. name의 값으로는 member라는 매개변수로 전달 받은 각 요소값들(즉, 각 멤버를 나타내는 이름 또는 아이디)을 주고, key의 값으로는 Math.random 메서드를 이용하여 랜덤한 숫자값을 주었습니다.

그런 다음 Array.prototype.sort 메서드를 사용하여 key 값을 기준으로 하여 오름차순으로 요소를 정렬했습니다. 그리고 이렇게 정렬된 순서 그대로, 원래처럼 객체가 아닌 문자열(각 멤버를 나타내는 이름 또는 아이디)을 요소로 갖는 배열로 돌려놓았습니다. 그리고 Array.prototype.slice 메서드를 사용하여 이를 팀 당 인원 수에 맞게 자르고, 이렇게 부분복사해낸 배열들(각 팀)을 요소로 갖는 배열을 최종적으로 반환하도록 했습니다.

 

셋째 날

  • 이렇게 작성해놓은 코드를 적용한 명령어를 아래와 같이 작성했습니다.
const makeTeam = require('../../utils/teamMaker');

/* ---------------------------- random team maker --------------------------- */
exports.run = async (client, msg, args, prefix) => {
  // Map.prototype.forEach 메서드를 이용하여 현재 길드의 채널을 순회하며,
  // 메시지 작성자가 속해있는 음성채널 찾기
  let voiceChId = '';
  msg.guild.channels.cache.forEach((ch, chId) => {
    if (ch.type !== 'voice') return;

    for (let [memberId] of ch.members) {
      if (memberId === msg.author.id) {
        voiceChId = chId;
        return;
      }
    }
  });

  const channel = client.channels.cache.get(voiceChId);
  // 대상 음성 채널의 id를 인자로 전달

  try {
    if (!channel) { // 메시지 작성자가 음성 채널에 들어가지 않은 채 명령어를 입력했을 경우
      return await msg.reply(
        '음성 채널에 입장하신 뒤 팀짜기 명령어를 입력해주세요.'
      );
    }
  
    if (!args[0]) {
      await msg.reply(
        '한 팀당 인원 수를 함께 적어주세요. \n```\n!팀짜기 <한 팀당 인원 수>\n\n(ex: !팀짜기 3)```'
      );
  
      return;
    }
  
    if (Object.is(+args[0], NaN) || Math.floor(+args[0]) === 0) {
      await msg.reply(
        '올바른 숫자를 입력해주세요. \n```\n!팀짜기 <한 팀당 인원 수>\n\n(ex: !팀짜기 3)```'
      );
  
      return;
    }
  
    let membersArray = [];
  
    for (let member of channel.members) {
      membersArray.push('<@' + member[1].user.id + '>'); // 봇이 해당 사용자를 맨션하도록
    }
 
  
    const teamArray = makeTeam(membersArray, Math.floor(+args[0]));
  
    const result = teamArray.map((team, idx) => `${idx + 1}팀: ${team.join(', ')}`);
  
    await msg.channel.send(result.join('\n'));
  } catch (err) { // 명령어 실행 도중 에러가 발생할 경우 에러 메시지 출력
    console.error(err);
  }
};

exports.config = {
  name: '팀짜기',
  aliases: ['team', '팀'],
  category: ['bot'],
  des: ['사용자와 같은 음성 채널에 있는 멤버들을 대상으로 랜덤으로 팀을 정합니다.'],
  use: ['!팀짜기 <한 팀당 인원 수>']
};

 

처음 계획은, 우선 메시지를 전송한 사용자가 들어가 있는 음성채널을 찾고, 해당 음성채널에 들어와 있는 전체 사용자를 대상으로 팀짜기 로직을 돌리는 방식으로 설계를 했었고, 이대로 구현해보려고 했으나....

Teammy 권한 문제인지 아니면 제가 구글링과 공식 문서를 충분히 찾아보지 못한 문제인지는 모르겠으나 결과적으로는 채팅채널에서 메시지를 보낸 사용자가 속해있는 음성채널에 대한 정보를 바로 출력해주는 메서드는 찾지 못했습니다... 그래서 결국 저런 식으로 길드 내의 음성채널들을 훑으며 각 음성채널에 현재 입장해있는 멤버 목록에서 해당 사용자가 있는지를 찾아보는 상대적으로 비효율적인 코드를 작성할 수 밖에 없었습니다.. ㅠ _ ㅠ 구글링과 공식 문서에 나와있는 메서드들을 시도해봤는데 왜 때문인지 음성채널에 대한 정보만 쏙 빠진 채 출력이 되더라구요...? 추후 유지보수를 하며 이에 대한 원인이 무엇인지 알아낸다면 추가로 포스팅하도록 하겠습니다.. (혹시나 이런 경험을 하셨거나 원인을 알 것 같으신 분들은 알려주시면 감사하겠습니다!!)

 

<코드 설명>

- 메시지(팀짜기 명령어)가 전송된 길드(=디스코드 서버)에 존재하는 모든 채널(카테고리 채널, 음성채널, 채팅채널)을 순회하며, 우선은 음성채널이 아닐 경우에는 다음 채널로 넘어갑니다. 음성채널이 맞을 경우에는, 채널에 입장해있는 멤버들의 목록을 순회합니다. 이때 각 멤버의 정보는 배열 형태로 되어 있는데요, 배열 디스트럭처링 할당(=배열 구조 분해 할당)을 사용하여 각 멤버 정보 중 0번 인덱스에 해당하는 멤버 id만 추출을 해서 memberId라는 변수에 담습니다. 멤버 id는 517805308513615900 이런 식으로 18자리의 숫자로 이루어져있습니다. 그리고 이 memberId 값과 팀짜기 명령어 작성자의 id 값이 일치할 경우에는 voiceChId라는 변수에 해당 음성채널의 id를 할당합니다. 그럼 이제 forEach문으로 순회를 다 마친 시점에서는 voiceChId 변수의 값이 빈 문자열 또는 어떤 특정 음성채널의 id 값이겠죠?? 이제 이 voiceChId 값을 Map.prototype.get 메서드의 인자로 전달을 해서 채널 캐시로부터 해당 음성채널의 정보를 받아오고 이를 channel이라는 변수에 할당을 해줍니다. 만약 voiceChId의 값이 빈 문자열이라면 channel의 값은 undefined가 됩니다.

 

- 디스트럭처링 할당은 ES6 문법 중 하나로, 이터러블이나 객체로부터 필요한 값만 뽑아서 변수에 할당하는 경우 유용하다고 합니다. 배열 디스트럭처렁 할당을 사용할 경우에는 할당의 대상(=할당문의 우변)이 반드시 이터러블(iterable)이어야 하며 이터러블이 아닐 경우에는 에러가 발생합니다. 이터러블이란 순회 가능한 자료 구조로, 배열, 문자열, Map, Set 등을 포함한다고 하네요.

// ES6 배열 디스트럭처링 할당
const arr = [1, 2, 3];

// 변수 one, two, three를 선언하고 배열 arr을 디스트럭처링하여 할당한다.
// 이때 할당 기준은 배열의 인덱스다.
const [one, two, three] = arr;
// 이처럼 변수는 배열 리터럴 형태로 선언해야 디스트럭처링 할당됨.

console.log(one, two, three); // 1 2 3

- 이제 사용자의 메시지, 즉 명령어 내용에 따라 예외적인 경우 3개와 명령어를 정상적으로 입력했을 경우 1개 해서 총 4개의 경우로 나누어서 코드를 작성해봤습니다.

  1. 명령어 작성자가 음성채널에 입장하지 않은 상태로 명령어를 입력했을 경우

  2. !팀짜기만 입력하고, 각 팀당 인원수는 전달하지 않은 경우

  3. !팀짜기 ㅁㄴㅇㄹ, !팀짜기 0과 같이 각 팀당 인원수를 1 이상의 숫자로 전달하지 않은 경우

  4. 명령어를 정상적으로 입력했을 경우

 

  • 이제 임시 데이터로 테스트를 해볼까요? 티미를 테스트하고 있는 채널에 저밖에 없으므로... 제 id 하나를 가지고 맨 뒤에 0부터 11까지의 정수를 문자열에 더해서 순서가 랜덤하게 섞이는지 확인해보도록 하겠습니다~
  • 먼저 정상적으로 명령어를 입력한 경우부터 확인해보겠습니다.

테스트 결과

  • 매번 순서가 불규칙적으로 정해지는 것을 보니 제대로 돌아가는 것 같네요~
  • 그럼 이제 예외 처리도 잘 되는지 확인해보겠습니다 ㅎㅎ

테스트 결과

 

다음 시간에는 Heroku를 이용한 배포 과정에 대해 정리해보겠습니다~~

 

 

복사했습니다!