백준 관련해서 디스코드 봇을 만들고 있는데, 만드는 도중 몇 가지 상황에 대해서 유저 입력을 받아야 할 때, 어떻게 처리해야 하는지 고민이었다. 특히, 디스코드 봇을 아예 처음 제작하는 상황이라 맨땅에 헤딩 수준으로 들이 박아야 했다.
뭘 되게 많이 쓰긴 했지만, 그냥 요약하면 다음과 같다.
"디코 봇이랑 티키타카 어떻게 함?"
이게 말만 들으면 "엥? 그냥 보통 서버에서도 이벤트 듣는 .listen() 처럼 대충 비슷한 기법으로 하면 되는 거 아님?" 이럴 수 있지만, 잘 생각해야 하는게, 디스코드 봇 서버는 하나만 돌아가고 디코봇을 사용한 사람은 여러명이다.
한마디로, 디코봇에게 메시지를 보낸 사람과 메시지 내용을 구별할 줄 알아야 하고, 그에 따른 응답을 할 줄 알아야 한다. 자, 그럼 한 가지 질문이 더 있다.
"디코 봇은 사용자가 말할 때까지 어떻게 기다리지?"
상황을 가정하면 다음과 같다.
이걸 이제 어떻게 하느냐 이말이다. 웹 서버라면 사용자의 ip를 바탕으로 소캣을 이용하여 listen()을 통해 하면 될 것이다. 근데 이건 '디코봇'이다. 웹으로 뭘 띄우는 게 아니라 디스코드를 통해 채팅을 해야 한다는 것이다. 과연, 단일 서버가 이걸 처리할 수 있을까? 다행히 discord.js는 놀랍게도 이를 구현하는 메소드가 있다.
10.18) 지금 다시 읽어보니, 저 밑에 말한 Collectors도 그냥 소켓의 일종이다...ㅋㅋ...애초에 디스코드 봇이 웹소켓 방식으로 동작하는 것이니...글은 이미 썼으니, 밑의 설명들은 그냥 소켓의 설명이라고 생각하면 된다..
답은 Collectors를 이용해 사용자의 응답을 대신 듣는 객체를 디스코드 서버에 생성하는 것이다. 그림을 그리면 다음과 같다.

A와 B가 동시에 '!아이디등록' 이라는 명령어를 입력하면, 디스코드 봇은 각 사용자의 채팅 내용을 대신 기록할 MessageCollector 객체를 생성한다. MessageCollector은 이름 그대로 메시지를 수집하는 클래스다. 만일 각 Collector가 메시지를 수집하면, Collector은 채팅 내용을 반환한다.
코드를 한번 보자. 다음은 개발 중인 백준 봇의 코드 중, 사용자의 백준 ID를 입력하는 코드의 일부다.
async function registerId(conn, message) {
message.reply("아이디를 입력해주세요.");
const botFilter = m => !m.author.bot && m.author.id === message.author.id && !m.content.startsWith('!');
const idCollector = message.channel.createMessageCollector({filter: botFilter,max:1, time: 20000});
idCollector.on('collect', async msg => {
const bojId = msg.content;
const response = discord_util.addBojId(conn, message.author.id, bojId);
if (response){
message.reply("정상적으로 등록되었습니다.")
}else{
message.reply("알 수 없는 오류가 발생했습니다.")
}
})
idCollector.on('end', collected => {
if (collected.size === 0){
message.reply("입력 시간이 만료되었습니다.")
}
})
}
아까 말했던 MessageCollector 객체가 쓰인 곳을 천천히 뜯어보겠다.
const botFilter = m => !m.author.bot && m.author.id === message.author.id && !m.content.startsWith('!');
const idCollector = message.channel.createMessageCollector({filter: botFilter,max:1, time: 20000});
MessageCollector은 기본적으로 메시지 필터링 기능이 있다. 즉, 위에 있는 그림에서 '특정 누군가의 메시지'만을 수집하려고 할 때 쓰인다. 물론, 특정 메시지 만을 수집할 때도 쓰일 수 있다.
botfilter 변수는 필터링 변수로, 봇 자체가 보내는 메시지를 무시하고, 메시지를 (채팅을) 보낸 사용자와 명령어를 입력한 사용자의 ID가 같을 때만 메시지를 수집한다. 뒤에는 다른 명령어 (!로 시작하는 명령어)를 무시하기 위해 쓴 조건문이다.
그 아래에는 MessageCollector을 디스코드 서버 스레드에 생성하기 위해 message.channel.createMessageCollector()를 사용하고, 파라미터로 위에서 생성한 필터링 변수와, 최대 얼만큼 메시지를 수집할 건지 (max:1), 그리고 얼마동안 대기할 것인지 (time: 20000)를 넘겨준다.
그렇게 되면 디스코드 봇에 명령어를 입력할 때 디스코드 봇에 MessageCollector 객체가 생성된다. 작동 모습을 보자.

!register을 입력하면 저렇게 이미 등록되었다고 나오고 저 셋 중 하나를 입력하라고 한다. 이 순간, 디스코드 봇은 MessageCollector을 하나 생성한 것이다. 그럼 여기서 쓰인 필터링 변수는 뭐였을까?
당연히 '변경', '삭제', '취소'만을 응답으로 받도록 필터링 변수가 선언되었을 것이다. 이렇게 말이다.
const botFilter = m => !m.author.bot &&
m.author.id === message.author.id &&
!m.content.startsWith('!') &&
(m.content === '변경' || m.content === '삭제' || m.content === '취소');
웃긴건, 만약 !m.author.bot조건을 안 넣어주면 지가 묻고 지가 답하는 웃긴 상황이 벌어진다. 어쨌든 봇이 보낸 메시지 또한 메시지로 인식되기 때문이다. 이 코드 때문에 그렇다.
client.on('messageCreate', message => {
...
}
이건 디코봇이 디스코드 스레드 내에서 메시지가 생성되었을 때 발생하는 이벤트다. 디코봇은 디스코드 스레드에서 메시지 생성 이벤트를 감지하고 코드 내용을 실행한다. 어쨌든, 봇이 보낸 메시지는 꼭 무시하도록 하자.
이어서 작동 상황을 보겠다.

아이디를 입력하라 해서 잘 입력헀더니 정상적으로 변경이 되었다고 한다. 자, 문제를 하나 주겠다. 저기 "synoti211" 메시지는 누가 감지한 것일까? 디스코드 봇일까, 아니면 MessageCollector일까?
내 글을 잘 읽었다면 주저없이 MessageCollector을 택했을 것이다. 내가 말했듯, 디스코드 봇은 자기 대신 메시지를 들어줄 객체로 MessageCollector를 생성한다고 했다. 따라서, 이 MessageCollector 역시 event listener이 존재한다.
idCollector.on('collect', async msg => {
const bojId = msg.content;
const response = discord_util.addBojId(conn, message.author.id, bojId);
if (response){
message.reply("정상적으로 등록되었습니다.")
}else{
message.reply("알 수 없는 오류가 발생했습니다.")
}
})
idCollector.on('end', collected => {
if (collected.size === 0){
message.reply("입력 시간이 만료되었습니다.")
}
})
idCollector.on('collect')는 MessageCollector 객체가 메시지를 수집하는 이벤트가 발생됐을 때, 즉 콜렉터가 메시지를 수집했을 때 실행되는 함수다. 그렇게 함수가 실행되면 사용자에게 정상적으로 등록되었다고 답장을 보낸다.
또한, idCollector.on('end')는 MessageCollector이 메시지 수집을 완료했을 때 실행되는 함수다. 이는 위에서 설명한 필터링 변수에서 timeout을 설정했을 때도 실행된다. 이렇게 말이다.

지금까지 글을 잘 읽었는지 확인하기 위해 문제를 주겠다. 다음 상황에서 각각의 메시지는 누가 보낸 걸까?

"알 수 없는 명령어입니다."는 디스코드 봇이, "입력 시간이 만료되었습니다"는 MessageCollector이 보낸 것이다. 그 이유는 아까 설명했던 botfilter에 있다.
const botFilter = m => !m.author.bot &&
m.author.id === message.author.id &&
!m.content.startsWith('!') &&
(m.content === '변경' || m.content === '삭제' || m.content === '취소');
이 필터링 변수에는 !m.content.startsWith('!')라는 조건문이 있다. 즉, !로 시작하는 명령어는 듣지 않겠다는 것이다. 그럼 저 답장은 어떻게 된걸까? 저 메시지는 MessageCollector이 아닌, 디스코드 봇 메인 객체client가 보낸 것이다. 
그림을 다시 보면 이해가 될 것이다. 디스코드 봇은 Collector 객체를 생성하고, 채팅을 수집하는 역할을 위임했다. 즉, 디스코드 봇은 사용자의 채팅을 수집하는 역할을 하지 않고, 그 외의 기능을 수행하고 있었기에, 만약 명령어가 이상한 명령어일 경우 경고를 보내는 자체 함수가 실행된 것이다.
지금까지 내가 디스코드 봇을 만들면서 티키타카를 이루기 위해 사용했던 MessageCollector에 대해 설명했다. 이 글을 읽는 사람도 자신만의 디스코드 봇을 만들 때 티키타카가 필요한 경우, 유용하게 사용하기 바란다.