[DiscordJS 봇 개발 튜토리얼] 5. 임베드 메시지와 버튼 만들기: 봇과의 소통을 더 풍부하게!

2025. 6. 6. 17:12·DiscordJS 개발 튜토리얼

해당 글은, 제가 작성한 Discord.js 보일러플레이트를 기반으로 합니다. 해당 보일러픝레이트는 다음에라도 봇을 빠르게 만들고 싶으실 때 사용하실 수 있습니다. Discord.js TypeScript Boilerplate

안녕하세요! 지난 시간에는 명령어에 쿨타임을 설정하고 예상치 못한 오류를 효과적으로 처리하는 방법을 배웠습니다. 덕분에 우리 봇은 한층 더 안정적이고 사용자 친화적으로 발전했네요.

이번 시간에는 봇이 보내는 메시지를 훨씬 더 보기 좋고 다채롭게 만들어주는 임베드(Embed) 메시지와, 사용자와의 상호작용을 한 단계 끌어올릴 수 있는 버튼(Button) 컴포넌트에 대해 알아보겠습니다. 단순한 텍스트 응답을 넘어, 봇과의 대화가 더욱 즐거워질 준비, 되셨나요?

평범한 메시지는 이제 그만! 임베드(Embed) 메시지 활용하기

임베드 메시지는 디스코드에서 일반 텍스트 메시지보다 훨씬 풍부한 정보를 담을 수 있는 특별한 형식의 메시지입니다. 제목, 설명, 색상, 이미지, 썸네일, 필드, 푸터 등 다양한 요소를 활용하여 마치 잘 디자인된 카드처럼 정보를 표현할 수 있습니다.

봇이 중요한 정보를 전달하거나, 명령어의 결과를 깔끔하게 보여주고 싶을 때 임베드는 아주 강력한 도구가 됩니다. discord.js에서는 EmbedBuilder 클래스를 사용하여 쉽게 임베드를 만들고 커스터마이징할 수 있습니다.

EmbedBuilder 기본 사용법

EmbedBuilder는 여러 메서드를 체이닝 방식으로 호출하여 임베드의 각 부분을 설정합니다.

  • setTitle(title): 임베드의 제목을 설정합니다.
  • setDescription(description): 임베드의 주요 내용을 설정합니다. Markdown도 지원합니다.
  • setColor(color): 임베드 좌측에 표시될 세로줄의 색상을 설정합니다. 16진수 색상 코드, 특정 색상 이름 (예: 'Random', 'Blue') 등을 사용할 수 있습니다.
  • setAuthor({ name, iconURL, url }): 임베드 상단에 작성자 정보를 표시합니다. 이름, 아이콘 URL, 클릭 시 이동할 URL을 설정할 수 있습니다.
  • setThumbnail(url): 임베드 우측 상단에 작은 이미지를 표시합니다.
  • setImage(url): 임베드 본문에 큰 이미지를 표시합니다.
  • addFields(...fields): 여러 개의 필드를 추가합니다. 각 필드는 { name: string, value: string, inline?: boolean } 형태의 객체입니다. inline: true로 설정하면 여러 필드가 한 줄에 나란히 표시될 수 있습니다.
  • setFooter({ text, iconURL }): 임베드 하단에 푸터 텍스트와 아이콘을 표시합니다.
  • setTimestamp(timestamp?): 임베드 푸터 옆에 타임스탬프를 표시합니다. 인자를 생략하면 현재 시간으로 설정됩니다.
  • setURL(url): 제목을 클릭했을 때 이동할 URL을 설정합니다.

예제: /정보 명령어에 임베드 적용하기

지난 시간에 만들었던 /정보 서버 명령어를 임베드를 사용하여 더 보기 좋게 만들어 봅시다.

  1. 명령어 파일 수정: src/commands/info.ts 파일을 열고 execute 함수 내 서버 서브커맨드 부분을 수정합니다.

    // src/commands/info.ts
    import { SlashCommandBuilder, ChatInputCommandInteraction, Guild, EmbedBuilder } from 'discord.js'; // EmbedBuilder 추가
    
    // ... (data 정의는 동일) ...
    
    export async function execute(interaction: ChatInputCommandInteraction) {
        const subcommand = interaction.options.getSubcommand();
    
        if (subcommand === '사용자') {
            // ... (사용자 정보 로직 - 이전과 동일하게 두거나, 여기도 임베드로 개선 가능) ...
            const user = interaction.options.getUser('대상', true);
            const member = interaction.guild?.members.cache.get(user.id);
    
            if (!member) {
                await interaction.reply({ content: '해당 사용자를 찾을 수 없습니다.', ephemeral: true });
                return;
            }
    
            const userEmbed = new EmbedBuilder()
                .setColor('Random') // 랜덤 색상
                .setTitle(\`👤 \${user.username}님의 정보\`)
                .setThumbnail(user.displayAvatarURL({ dynamic: true })) // 동적 프로필 이미지 (gif 등 지원)
                .addFields(
                    { name: 'ID', value: user.id, inline: true },
                    { name: '태그', value: user.tag, inline: true },
                    { name: '계정 생성일', value: user.createdAt.toLocaleDateString('ko-KR'), inline: false },
                    { name: '서버 참여일', value: member.joinedAt ? member.joinedAt.toLocaleDateString('ko-KR') : '알 수 없음', inline: false }
                )
                .setTimestamp()
                .setFooter({ text: \`요청자: \${interaction.user.username}\`, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) });
    
            await interaction.reply({ embeds: [userEmbed] });
    
        } else if (subcommand === '서버') {
            const guild = interaction.guild as Guild;
    
            if (!guild) {
                await interaction.reply({ content: '서버 정보를 가져올 수 없습니다. (DM에서는 사용 불가)', ephemeral: true });
                return;
            }
    
            await guild.members.fetch(); // 멤버 수 정확히 가져오기
            const owner = await guild.fetchOwner();
    
            const serverEmbed = new EmbedBuilder()
                .setColor(0x0099FF) // 16진수 색상 코드 (파란색 계열)
                .setTitle(\`🏰 \${guild.name} 서버 정보\`)
                .setThumbnail(guild.iconURL({ dynamic: true })) // 서버 아이콘
                .addFields(
                    { name: 'ID', value: guild.id, inline: true },
                    { name: '서버 주인', value: owner.user.tag, inline: true },
                    { name: '멤버 수', value: \`총 \${guild.memberCount}명\`, inline: true }, // (온라인: \${guild.presences.cache.filter(p => p.status !== 'offline').size}명) 등 추가 가능
                    { name: '채널 수', value: \`텍스트: \${guild.channels.cache.filter(c => c.isTextBased()).size}개, 음성: \${guild.channels.cache.filter(c => c.isVoiceBased()).size}개\`, inline: false },
                    { name: '역할 수', value: \`\${guild.roles.cache.size}개\`, inline: true },
                    { name: '이모지 수', value: \`\${guild.emojis.cache.size}개\`, inline: true },
                    { name: '생성일', value: guild.createdAt.toLocaleDateString('ko-KR'), inline: false }
                )
                .setTimestamp()
                .setFooter({ text: \`요청자: \${interaction.user.username}\`, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) });
    
            await interaction.reply({ embeds: [serverEmbed] });
        }
    }
  2. 봇 실행 및 테스트: 봇을 재시작하고 디스코드에서 /정보 서버와 /정보 사용자 명령어를 실행해보세요. 이전보다 훨씬 깔끔하고 보기 좋은 형태로 정보가 표시될 겁니다!

    응답을 보낼 때는 interaction.reply()의 embeds 속성에 EmbedBuilder 인스턴스를 배열로 담아 전달합니다. ({ embeds: [myEmbed] })

사용자와의 다음 단계: 버튼 (Buttons) 추가하기

버튼은 사용자가 메시지 아래에 있는 버튼을 클릭하여 봇과 상호작용할 수 있게 하는 기능입니다. 예를 들어, "예/아니오" 선택, 페이지 넘기기, 특정 작업 실행 등을 버튼으로 구현할 수 있습니다.

discord.js에서는 ButtonBuilder를 사용하여 버튼을 만들고, ActionRowBuilder를 사용하여 이 버튼들을 한 줄에 배치합니다. 버튼 클릭 이벤트는 Events.InteractionCreate 이벤트 핸들러에서 interaction.isButton()으로 감지하여 처리합니다.

ButtonBuilder 기본 사용법

  • setCustomId(id): 버튼을 식별하는 고유 ID를 설정합니다. 이 ID를 통해 어떤 버튼이 클릭되었는지 구분합니다.
  • setLabel(label): 버튼에 표시될 텍스트를 설정합니다.
  • setStyle(style): 버튼의 스타일(색상)을 설정합니다. ButtonStyle 열거형 값을 사용합니다.
    • ButtonStyle.Primary: 파란색 (기본)
    • ButtonStyle.Secondary: 회색
    • ButtonStyle.Success: 초록색
    • ButtonStyle.Danger: 빨간색
    • ButtonStyle.Link: URL로 이동하는 링크 버튼 (이 경우 setURL()도 필요)
  • setEmoji(emoji): 버튼에 이모지를 추가합니다.
  • setDisabled(disabled): 버튼을 비활성화할지 여부를 설정합니다. (기본값: false)
  • setURL(url): ButtonStyle.Link 스타일일 때, 클릭 시 이동할 URL을 설정합니다.

예제: /투표 명령어 만들기 (버튼 활용)

간단한 찬반 투표 기능을 가진 /투표 명령어를 만들어 봅시다. 사용자가 투표 주제를 입력하면, 봇이 임베드 메시지와 함께 "찬성"과 "반대" 버튼을 표시하고, 각 버튼이 클릭될 때마다 콘솔에 로그를 남기는 예제입니다. (실제 투표 수 집계는 다음 단계에서 다룰 수 있습니다.)

  1. 명령어 파일 생성: src/commands/vote.ts 파일을 새로 만듭니다.

    // src/commands/vote.ts
    import {
        SlashCommandBuilder,
        ChatInputCommandInteraction,
        EmbedBuilder,
        ButtonBuilder,
        ButtonStyle,
        ActionRowBuilder,
        ComponentType // ActionRowBuilder에 버튼을 담을 때 필요
    } from 'discord.js';
    
    export const data = new SlashCommandBuilder()
        .setName('투표')
        .setDescription('간단한 찬반 투표를 시작합니다.')
        .addStringOption(option =>
            option.setName('주제')
                .setDescription('투표할 주제를 입력해주세요.')
                .setRequired(true));
    
    export async function execute(interaction: ChatInputCommandInteraction) {
        const topic = interaction.options.getString('주제', true);
    
        const voteEmbed = new EmbedBuilder()
            .setColor(0x5865F2) // 디스코드 보라색
            .setTitle(\`🗳️ 투표: \${topic}\`)
            .setDescription('아래 버튼을 눌러 찬성 또는 반대 의견을 표시해주세요!')
            .setTimestamp()
            .setFooter({ text: \`투표 시작자: \${interaction.user.username}\`, iconURL: interaction.user.displayAvatarURL() });
    
        // 버튼 생성
        const approveButton = new ButtonBuilder()
            .setCustomId('vote_approve') // 각 버튼을 구별할 고유 ID
            .setLabel('찬성 👍')
            .setStyle(ButtonStyle.Success); // 초록색 버튼
    
        const disapproveButton = new ButtonBuilder()
            .setCustomId('vote_disapprove')
            .setLabel('반대 👎')
            .setStyle(ButtonStyle.Danger); // 빨간색 버튼
    
        // ActionRowBuilder에 버튼들을 추가 (한 줄에 최대 5개까지 가능)
        // ActionRowBuilder<ButtonBuilder> 타입 명시
        const row = new ActionRowBuilder<ButtonBuilder>()
            .addComponents(approveButton, disapproveButton);
    
        await interaction.reply({
            embeds: [voteEmbed],
            components: [row] // components 속성에 ActionRow 배열을 전달
        });
    }
  2. 명령어 등록: src/commands/index.ts 파일에 vote 명령어를 추가합니다.

    // src/commands/index.ts
    import * as ping from "./ping";
    import * as greet from "./greet";
    import * as info from "./info";
    import * as vote from "./vote"; // vote 명령어 import 추가
    
    export const commands = {
      ping,
      greet,
      info,
      vote, // commands 객체에 vote 추가
    };
  3. 버튼 상호작용 처리: src/index.ts 파일의 InteractionCreate 이벤트 핸들러를 수정하여 버튼 클릭을 감지하고 처리합니다.

    // src/index.ts (일부 발췌)
    // ... (client, commands 등 기존 코드) ...
    
    client.on(Events.InteractionCreate, async (interaction) => {
        try {
            if (interaction.isChatInputCommand()) {
                const command = commands[interaction.commandName as keyof typeof commands];
                if (!command) {
                    console.error(\`No command matching \${interaction.commandName} was found.\`);
                    await interaction.reply({ content: '알 수 없는 명령어입니다.', ephemeral: true });
                    return;
                }
                await command.execute(interaction);
    
            } else if (interaction.isButton()) {
                // 버튼 상호작용 처리
                const customId = interaction.customId;
    
                if (customId === 'vote_approve') {
                    // ephemeral: true 로 설정하면 클릭한 사용자에게만 보임
                    await interaction.reply({ content: '찬성표를 던졌습니다!', ephemeral: true });
                    console.log(\`\${interaction.user.tag}님이 '\${interaction.message.embeds[0]?.title}' 투표에 찬성했습니다.\`);
                } else if (customId === 'vote_disapprove') {
                    await interaction.reply({ content: '반대표를 던졌습니다!', ephemeral: true });
                    console.log(\`\${interaction.user.tag}님이 '\${interaction.message.embeds[0]?.title}' 투표에 반대했습니다.\`);
                }
                // 실제 투표 데이터를 저장하고 업데이트하는 로직은 여기에 추가될 수 있습니다.
                // 예를 들어, interaction.message.edit()을 사용하여 임베드나 버튼을 수정할 수 있습니다.
            }
            // 여기에 다른 타입의 상호작용(SelectMenu, Modal 등) 처리 로직을 추가할 수 있습니다.
    
        } catch (error) {
            console.error('Error handling interaction:', error);
            if (interaction.isRepliable()) {
                const replyMethod = interaction.replied || interaction.deferred ? 'followUp' : 'reply';
                await interaction[replyMethod]({
                    content: '명령어 처리 중 알 수 없는 오류가 발생했습니다.',
                    ephemeral: true
                }).catch(e => console.error('오류 응답 전송 실패:', e));
            }
        }
    });
    
    // ... (봇 로그인) ...

    중요: 버튼 클릭에 대한 응답(`interaction.reply`, `interaction.update`, `interaction.deferUpdate` 등)은 반드시 3초 이내에 이루어져야 합니다. 그렇지 않으면 상호작용 실패로 간주됩니다. 만약 응답 준비에 시간이 더 걸린다면, 먼저 `interaction.deferUpdate()` (버튼 상태는 그대로 두고 로딩 상태만 표시) 또는 `interaction.deferReply()` (새로운 메시지로 응답할 것임을 알리고 로딩 상태 표시)를 호출해야 합니다.

  4. 봇 실행 및 테스트: 봇을 재시작하고 /투표 주제:새로운 기능 추가 와 같이 명령어를 실행해보세요. 임베드 메시지와 함께 찬성/반대 버튼이 나타나고, 버튼을 클릭하면 콘솔에 로그가 찍히고 사용자에게 ephemeral 메시지가 표시될 겁니다.

마무리하며

오늘은 EmbedBuilder를 사용하여 정보를 시각적으로 아름답게 표현하는 방법과, ButtonBuilder 및 ActionRowBuilder를 활용하여 사용자와의 상호작용을 한층 끌어올리는 버튼 컴포넌트를 만드는 방법을 배웠습니다. 이제 여러분의 봇은 단순한 텍스트를 넘어, 풍부한 UI를 통해 사용자와 소통할 수 있게 되었습니다.

다음 시간에는 디스코드 봇 개발에서 빼놓을 수 없는 중요한 부분인 이벤트 핸들링에 대해 더 깊이 파고들어 보겠습니다. 단순히 명령어 입력(`InteractionCreate`)뿐만 아니라, 메시지 수정, 삭제, 멤버의 서버 참여/퇴장 등 다양한 서버 내 활동들을 감지하고 그에 맞는 동작을 수행하는 방법을 배우게 될 겁니다. 봇이 더욱 능동적으로 서버 환경에 반응하도록 만들어 봅시다!

'DiscordJS 개발 튜토리얼' 카테고리의 다른 글

[DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리  (1) 2025.06.05
[DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!  (1) 2025.06.04
[DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음  (1) 2025.06.03
[DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기  (2) 2025.06.02
[DiscordJS 봇 개발 튜토리얼] 0. 프로젝트, 봇 생성하기  (3) 2025.06.01
'DiscordJS 개발 튜토리얼' 카테고리의 다른 글
  • [DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리
  • [DiscordJS 봇 개발 튜토리얼] 3. 슬래시 명령어: 옵션과 서브커맨드로 더욱 강력하게!
  • [DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음
  • [DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기
디스호스트
디스호스트
쉽고 안정적인 디스코드 봇 호스팅 서비스, 디스호스트의 기술 블로그입니다. 디스호스트는 24시간 구동되는 서버를 통해 디스코드 봇을 대신 구동시켜 드리는 서비스를 제공하고 있습니다.
  • 디스호스트
    디스호스트 기술 블로그
    디스호스트
  • 블로그 메뉴

    • 홈
    • 디스호스트 사용 가이드
    • 디스코드 봇 호스팅, 24시간 서버 구동
    • 분류 전체보기 (33) N
      • 디스코드 (6)
      • 디스호스트 가이드 (11)
      • 봇 개발 팁 (10)
        • Discord.js (8)
        • Discord.py (1)
      • DiscordJS 개발 튜토리얼 (6) N
  • 링크

    • 디스호스트
    • 디스호스트 패널
  • hELLO· Designed By정상우.v4.10.3
디스호스트
[DiscordJS 봇 개발 튜토리얼] 5. 임베드 메시지와 버튼 만들기: 봇과의 소통을 더 풍부하게!
상단으로

티스토리툴바