MZ한 타입스크립트 스택으로 Discord 봇 만들고 명령어 등록하기

Tei·2023년 7월 14일
5
post-thumbnail

발단

사이드 프로젝트를 3명이서 진행하고 있는데, 일정을 관리(독촉)해주고 해당 주차 작업을 제대로 진행하지 않은 멤버에게 커피 구매권을 누적하는 봇이 있으면 좋겠다고 생각했다.

그래서 디스코드 봇을 만드는 방법을 찾아보던 중, 타입스크립트로 만드는 자료는 거의 없고, discord.js 문서는 뭔가 편안하게 읽히지 않으며 찾은 자료들은 2~3년은 지난듯한 코드밖에 없어 겸사겸사 정리해 보았다.

(공식 가이드)
"module.export요? MZ 하지 않군요"
https://discordjs.guide/creating-your-bot/slash-commands.html#individual-command-files

사용 라이브러리

  • yarn berry
  • esbuild (배포할때 도커 말아줄려고)
  • discord.js
  • typescript

봇 만들기

먼저, 디스코드 채널은 이미 생성되어있고, 권한을 가진 BOT 토큰도 이미 발급받았다는 전제 하에 시작한다.

프로젝트 셋업

MZ 개발자답게 yarn berry 를 설정해준다.

yarn set version berry

필요한 라이브러리들을 설치해준다. 디스코드 봇은 discord.js 라이브러리를 사용하여 만든다.
레포) https://github.com/discordjs
문서) https://discordjs.guide/#before-you-begin

yarn add -D typescript ts-node @types/node esbuild
yarn add discord.js

tsconfig도 적절하게 설정해준다. 아주 간단한 앱이기때문에 기본만 설정했다.

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
  },
  "include": ["./src/**/*"]
}

적당히 package.json 파일들도 작성해준다.

package.json

{
  "name": "test-bot",
  "packageManager": "yarn@3.6.1",
  "dependencies:"{
    "discord.js": "^14.11.0"
  },
  "devDependencies": {
    "@types/node": "^20.4.2",
    "ts-node": "^10.9.1",
    "typescript": "^5.1.6",
    "esbuild": "^0.18.12"
  },
  "scripts": {
    "start": "ts-node src/index.ts"
  }
}

봇 온라인 상태로 만들기

src/index.ts 파일에 아래와 같이 작성한다.

import {Client} from "discord.js";

const BOT_TOKEN = "MY_BOT_TOKEN"

const client = new Client({
    intents:  []
});

const startBot = async () => {
    await client.login(BOT_TOKEN);
    console.info("info: login success!")
}

startBot()

BOT_TOKEN 은 우아하게 환경변수로 관리해줘도 되겠지만, 다소 귀찮은 관계로 그냥 선언해주었다.

가볍게 yarn start 를 해주면

살아있는 봇을 확인할 수 있다.

이제 봇을 본격적으로 사용할 수 있도록 slash 명령어를 만들어보자.

명령어 타입 정의

types 디렉토리를 하나 만들고, slashCommand.ts 파일을 하나 생성한다.

src/types/slashCommand.ts

import { CommandInteraction, ChatInputApplicationCommandData, Client } from "discord.js";

export type SlashCommand = ChatInputApplicationCommandData &  {
    execute: (client: Client, interaction: CommandInteraction) => void;
}

이제 만들어준 타입을 사용해서 실제 명령어를 만들어보자.

명령어 만들기

봇에게 전달한 텍스트를 그대로 다시 말해주는 에코 명령어를 하나 만들어보자.
commands 디렉토리를 하나 만들고, echo.ts 파일을 하나 생성한다.

src/commands/echo.ts

import { ApplicationCommandOptionType } from "discord.js";
import {SlashCommand} from "../types/slashCommand";

export const echo: SlashCommand = {
    name: "메아리",
    description: "말을 그대로 따라합니다.",
    options:[
        {
            required:true,
            name:"뭐라고",
            description:"따라하게 시킬 내용을 적습니다",
            type:ApplicationCommandOptionType.String
        }
    ],
    execute: async (_, interaction) => {
        const echoMessage = (interaction.options.get("뭐라고")?.value || '');
        await interaction.followUp({
            ephemeral: true,
            content: `${interaction.user.username.toString()} 가라사대: ${echoMessage}`
        });
    }
};

위에서 정의해준 타입과 동일하게 구현해 준다.
name 은 등록될 명령어 이름, description 은 설명, 옵션은 유저가 입력할 수 있는 옵션을 의미한다.
아래 문서가 있는데, 약간 보기에 편하진 않게 되어있는 느낌이 있다. 타입스크립트로 작성하기 위해서는 타입을 열심히 뜯어봐야 하는 문제가 좀 있다.
(https://discordjs.guide/slash-commands/parsing-options.html#command-options)

commands/index.ts
만든 커맨드들을 export 해주자

import {echo} from "./echo";

const availableCommands = [echo]

export default availableCommands;

만든 명령어 등록하기

src/index.ts
startBot 을 수정해준다.


import commands from "./commands";

/*중간생략*/

const startBot = async () => {
    await client.login(BOT_TOKEN);
    console.info("info: login success!")

    client.on("ready",async ()=>{
        if(client.application){
            await client.application.commands.set(commands);
            console.log("info: command registered")
        }
    })
}

startBot()


잘 등록되었다.

디스코드에 접속해보면 "/" 를 입력했을 때 명령어가 잘 뜨는것을 확인할 수 있다.
자 그럼 실행해볼까?

아무 일도 벌어지지 않았다.

명령어를 등록은 했지만, 핸들러를 등록하지 않아서 그렇다.
자 그럼 핸들러도 마저 등록해보자.

명령어 핸들러 등록하기

slash 명령어가 동작하면 interactionCreate 이벤트가 발생한다.
해당 이벤트를 핸들링해주는 로직을 추가해주자.
src/index.ts

const startBot = async () => {
    await client.login(BOT_TOKEN);
    console.info("info: login success!")

    client.on("ready",async ()=>{
        if(client.application){
            await client.application.commands.set(commands);
            console.log("info: command registered")
        }
    })

  //핸들링 로직 추가
    client.on("interactionCreate", async (interaction: Interaction) => {
        if (interaction.isCommand()) {
          //등록한 명령어를 찾아서
            const currentCommand = commands.find(({name}) => name === interaction.commandName);

            if(currentCommand){
                await interaction.deferReply();
              //실행해준다. 
                currentCommand.execute(client, interaction);
                console.log(`info: command ${currentCommand.name} handled correctly`)
            }
        }
    });

}

아주 잘 동작하는걸 확인할 수 있다.

봇 배포하기

봇을 만든건 좋은데, 내 컴퓨터를 24시간 켜놓을수는 없다.
https://app.koyeb.com/
과 같은 무료 서비스에 올리기 위해 컨테이너로 말아주자.

esbuild 설정

esbuild로 말아서 올려주기 위해 esbuild.config.js 를 작성한다.
우리는 yarn berry 를 pnp로 사용하고있으니, esbuild-plugin-pnp 도 설치해주자.

yarn add -D @yarnpkg/esbuild-plugin-pnp

esbuild.config.js

const {build} = require('esbuild');
const { pnpPlugin } = require("@yarnpkg/esbuild-plugin-pnp") ;

build({
  entryPoints: ['./src/index.ts'],
  bundle: true,
  outfile: './dist/bot.js',
  platform: "node",
  plugins:[pnpPlugin()],
});

package.json 에도 빌드 스크립트를 추가해준다.

  "scripts": {
    "build": "node esbuild.config.js"
  }

arm 맥북을 사용중인 개발자라면 .yarnrc.yml 에 supportedArchitectures 도 추가해주자. koyeb 등의 서비스에서 빌드되는 과정에서 아키텍쳐 차이로 인한 오류가 발생할 수 있다.

supportedArchitectures:
  os:
    - "current"
    - "darwin"
    - "linux"
    - "win32"
  cpu:
    - "current"
    - "x86"
    - "x64"
    - "arm64"
    - "ia32"

Dockerfile

FROM node:latest as build
WORKDIR /app

COPY package.json ./
RUN yarn set version berry
COPY .yarn ./.yarn
COPY yarn.lock .yarnrc.yml ./
COPY esbuild.config.js ./
COPY src ./src
RUN yarn install
RUN yarn build

CMD [ "node", "./dist/bot.js" ]

컨테이너가 정상 빌드되어 봇이 잘 동작하는것을 확인할 수 있다.

간단한 헬스체크 추가

koyeb 이나 AWS 등의 서비스에서, 서비스가 죽었는지 헬스체크를 하고, 헬스체크를 통과하지 못하면 콘테이너를 죽인다. 우리 앱이 정상적으로 살아있는지 알려주기 위해서 200 응답을 주는 엔드포인트를 간단하게 작성해 줄 필요가 있다.

healthCheck.ts

export const healthCheck = createServer((request, response) => {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.write("OK");
    response.end();
});

index.ts

healthCheck.listen(8080) // 국룰 8080포트로 열어줬다.
startBot()

헬스체크를 통과한걸 확인할 수 있다.

마무리

간단하게 타입스크립트로 디스코드 봇을 만드는 방법을 알아보았다.
부디 타입스크립트를 사용하는 개발자들에게 도움이 되었으면 하는 바람이다.

profile
Being a service developer

1개의 댓글

comment-user-thumbnail
2023년 7월 15일

이야.. 역시 풀스택 디벨로퍼는 다르군요

답글 달기