해당 글은, 제가 작성한 Discord.js 보일러플레이트를 기반으로 합니다. 해당 보일러픝레이트는 다음에라도 봇을 빠르게 만들고 싶으실 때 사용하실 수 있습니다. Discord.js TypeScript Boilerplate
지난 시간에는 명령어들을 체계적으로 관리하기 위한 폴더 구조를 만들고, 첫 번째 슬래시 명령어인 /핑
을 등록해보았습니다. 이제 우리 봇은 단순한 텍스트 기반 명령어가 아닌, 디스코드 인터페이스에 깔끔하게 통합되는 슬래시 명령어를 사용할 준비가 되었죠.
이번 시간에는 여기서 한 걸음 더 나아가, 슬래시 명령어를 더욱 강력하고 유용하게 만들어주는 옵션(Options)과 서브커맨드(Subcommands)에 대해 자세히 알아보겠습니다. 사용자와 더 다양한 방식으로 상호작용하고, 복잡한 기능도 깔끔하게 구현할 수 있게 될 거예요.
슬래시 명령어에 날개를 달아주는 '옵션'
지금까지 만든 /핑
명령어는 사용자가 단순히 /핑
이라고 입력하면 정해진 응답을 보내는 방식이었습니다. 하지만 많은 경우, 명령어에 추가적인 정보를 함께 전달하고 싶을 때가 있습니다. 예를 들어, "/인사 [이름]"처럼 특정 사용자에게 인사하거나, "/검색 [키워드]"처럼 특정 내용을 검색하는 명령어를 만들고 싶을 수 있죠. 이럴 때 사용하는 것이 바로 '옵션'입니다.
SlashCommandBuilder
는 다양한 종류의 옵션을 추가할 수 있는 메서드들을 제공합니다.
addStringOption()
: 문자열 입력addIntegerOption()
: 정수 입력addNumberOption()
: 숫자 (정수 또는 실수) 입력addBooleanOption()
: 참/거짓 선택addUserOption()
: 사용자 멘션addChannelOption()
: 채널 선택addRoleOption()
: 역할 선택addMentionableOption()
: 사용자 또는 역할 멘션addAttachmentOption()
: 파일 첨부
각 옵션 메서드는 체이닝(chaining) 방식으로 설정할 수 있으며, 공통적으로 다음과 같은 세부 설정을 할 수 있습니다.
setName(name)
: 옵션의 이름 (코드에서 이 이름으로 값을 가져옴, 영어 소문자 권장)setDescription(description)
: 옵션에 대한 설명 (사용자에게 표시됨)setRequired(boolean)
: 이 옵션이 필수인지 아닌지 설정 (기본값은false
)
예제: 사용자에게 인사하는 명령어 만들기
그럼 /인사
라는 명령어를 만들고, user
타입의 옵션과 message
타입의 옵션을 추가해보겠습니다. user
옵션은 필수로, message
옵션은 선택으로 설정해볼게요.
src/commands/인사.ts
(또는 greet.ts
) 파일을 새로 만들고 아래와 같이 작성합니다.
// src/commands/인사.ts
import { SlashCommandBuilder, ChatInputCommandInteraction, User } from 'discord.js';
export const data = new SlashCommandBuilder()
.setName('인사')
.setDescription('지정한 사용자에게 인사 메시지를 보냅니다.')
.addUserOption(option =>
option.setName('대상') // 옵션 이름
.setDescription('인사할 대상을 선택하세요.') // 옵션 설명
.setRequired(true) // 필수 옵션으로 설정
)
.addStringOption(option =>
option.setName('메시지')
.setDescription('함께 전달할 메시지 (선택 사항)')
.setRequired(false) // 선택적 옵션
);
export async function execute(interaction: ChatInputCommandInteraction) {
// 옵션 값 가져오기
const targetUser = interaction.options.getUser('대상'); // '대상' 옵션에서 User 객체를 가져옴
const customMessage = interaction.options.getString('메시지'); // '메시지' 옵션에서 문자열을 가져옴
if (!targetUser) {
// 혹시 모를 경우에 대비 (setRequired(true)로 설정했으므로 보통은 null이 아님)
return interaction.reply({ content: '인사할 대상을 찾을 수 없어요.', ephemeral: true });
}
let replyMessage = \`\\${targetUser.toString()}님, 안녕하세요! 👋\\\`;
if (customMessage) {
replyMessage += `\\n> \\${customMessage}\`;
}
await interaction.reply(replyMessage);
}
코드 설명:
addUserOption(option => ...)
:대상
이라는 이름의 사용자 옵션을 추가합니다.setRequired(true)
로 필수 입력하도록 했습니다.addStringOption(option => ...)
:메시지
라는 이름의 문자열 옵션을 추가하고, 선택 사항으로 남겨둡니다.interaction.options.getUser('대상')
:execute
함수 내에서interaction.options
객체의getUser()
메서드를 사용해 '대상' 옵션으로 전달된User
객체를 가져옵니다. 다른 타입의 옵션도getString()
,getInteger()
,getBoolean()
등으로 가져올 수 있습니다.customMessage
가 있다면 인사 메시지에 추가합니다.
이 파일을 만들었다면, src/commands/index.ts
에 인사
명령어를 추가하고, npm run dev
(또는 npm start
)로 봇을 재시작하여 디스코드에 명령어가 잘 등록되는지 확인합니다. (boilerplate는 봇 시작 시 자동으로 명령어를 배포합니다.)
이제 디스코드에서 /인사
를 입력하면 대상
을 선택하는 필드가 나타나고, 선택적으로 메시지
도 입력할 수 있게 됩니다.
옵션에 선택지(Choices) 추가하기
때로는 사용자가 특정 값들 중에서만 선택하도록 하고 싶을 수 있습니다. 이럴 때 addChoices()
메서드를 사용합니다.
예를 들어, /음식추천
이라는 명령어를 만들고, 음식 종류를 '한식', '중식', '일식', '양식' 중에서 고르도록 해봅시다.
// src/commands/음식추천.ts
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
export const data = new SlashCommandBuilder()
.setName('음식추천')
.setDescription('선택한 종류의 음식을 추천해줍니다.')
.addStringOption(option =>
option.setName('종류')
.setDescription('원하는 음식 종류를 선택하세요.')
.setRequired(true)
.addChoices(
{ name: '🍚 한식', value: 'korean' },
{ name: '🍜 중식', value: 'chinese' },
{ name: '🍣 일식', value: 'japanese' },
{ name: '🍕 양식', value: 'western' }
)
);
export async function execute(interaction: ChatInputCommandInteraction) {
const foodType = interaction.options.getString('종류');
let recommendation = '';
switch (foodType) {
case 'korean':
recommendation = '오늘은 뜨끈한 국밥 어떠세요?';
break;
case 'chinese':
recommendation = '짜장면과 탕수육 조합은 진리죠!';
break;
case 'japanese':
recommendation = '신선한 초밥이나 라멘이 좋겠네요.';
break;
case 'western':
recommendation = '피자나 파스타로 든든하게 채워보세요!';
break;
default:
recommendation = '음... 뭘 먹어야 할까요?';
}
await interaction.reply(\`오늘의 추천 메뉴: \\${recommendation}\`);
}
addChoices()
에는 { name: '사용자에게 보여질 이름', value: '코드에서 사용할 값' }
형태의 객체 배열을 전달합니다. 사용자가 '🍚 한식'을 선택하면, 코드에서는 interaction.options.getString('종류')
의 결과로 'korean'
값을 받게 됩니다.
하나의 명령어, 여러 기능: 서브커맨드와 서브커맨드 그룹
명령어의 기능이 점점 복잡해지면, 관련된 여러 기능을 하나의 상위 명령어 아래에 묶고 싶을 때가 있습니다. 예를 들어, /정보
라는 명령어가 있고, 이 아래에 유저 정보 보기
, 서버 정보 보기
같은 하위 기능들을 두고 싶을 수 있죠. 이럴 때 사용하는 것이 서브커맨드(Subcommand)와 서브커맨드 그룹(Subcommand Group)입니다.
- 서브커맨드:
/명령어 subcommand
형태로 사용됩니다. (예:/정보 유저
) - 서브커맨드 그룹:
/명령어 group subcommand
형태로, 서브커맨드를 한 단계 더 그룹화할 수 있습니다. (예:/설정 메시지 알림
)
서브커맨드 만들기
SlashCommandBuilder
에서 addSubcommand()
메서드를 사용하여 서브커맨드를 정의할 수 있습니다. 각 서브커맨드는 자신만의 이름, 설명, 그리고 옵션을 가질 수 있습니다.
예제: /정보
명령어에 유저
와 서버
서브커맨드 만들기
// src/commands/정보.ts
import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember, Guild } from 'discord.js';
export const data = new SlashCommandBuilder()
.setName('정보')
.setDescription('사용자 또는 서버의 정보를 보여줍니다.')
.addSubcommand(subcommand =>
subcommand
.setName('유저')
.setDescription('선택한 사용자의 정보를 보여줍니다.')
.addUserOption(option => option.setName('대상').setDescription('정보를 볼 사용자').setRequired(true))
)
.addSubcommand(subcommand =>
subcommand
.setName('서버')
.setDescription('현재 서버의 정보를 보여줍니다.')
);
export async function execute(interaction: ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand(); // 사용된 서브커맨드의 이름을 가져옵니다.
if (subcommand === '유저') {
const user = interaction.options.getUser('대상');
if (!user) return interaction.reply({ content: '사용자를 찾을 수 없습니다.', ephemeral: true });
// 서버 멤버 정보를 가져오기 위해 interaction.member를 사용하거나, fetchMember를 고려할 수 있습니다.
const member = interaction.guild?.members.cache.get(user.id) || await interaction.guild?.members.fetch(user.id);
await interaction.reply(
`사용자 이름: \\${user.username}\\n` +
`ID: \\${user.id}\\n` +
(member instanceof GuildMember ? \`서버 합류일: \\${member.joinedAt?.toLocaleDateString()}\\n\` : '') +
`계정 생성일: \\${user.createdAt.toLocaleDateString()}`
);
} else if (subcommand === '서버') {
const guild = interaction.guild;
if (!guild) return interaction.reply({ content: '서버 정보를 가져올 수 없습니다. DM에서는 사용할 수 없어요.', ephemeral: true });
await interaction.reply(
`서버 이름: \\${guild.name}\\n` +
`총 멤버 수: \\${guild.memberCount}\\n` +
`서버 생성일: \\${guild.createdAt.toLocaleDateString()}\\n` +
`서버 주인: \\${(await guild.fetchOwner()).user.tag}`
);
}
}
코드 설명:
.addSubcommand(subcommand => ...)
: 각 서브커맨드를 정의합니다.유저
서브커맨드는대상
이라는 사용자 옵션을 추가로 받습니다.interaction.options.getSubcommand()
:execute
함수 내에서 어떤 서브커맨드가 사용되었는지 그 이름을 문자열로 가져옵니다.- 이후
if/else if
문을 사용하여 각 서브커맨드에 맞는 로직을 실행합니다.
서브커맨드 그룹 만들기 (간단 소개)
서브커맨드 그룹은 addSubcommandGroup()
메서드를 사용합니다. 그룹 안에 다시 addSubcommand()
를 사용하여 실제 서브커맨드들을 정의할 수 있습니다.
// 예시: /config group subcommand
new SlashCommandBuilder()
.setName("config")
.setDescription("봇 설정을 관리합니다.")
.addSubcommandGroup((group) =>
group
.setName("prefix")
.setDescription("접두사 관련 설정을 관리합니다.")
.addSubcommand((subcommand) =>
subcommand
.setName("set")
.setDescription("새로운 접두사를 설정합니다.")
.addStringOption((option) =>
option
.setName("value")
.setDescription("새 접두사")
.setRequired(true)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName("view")
.setDescription("현재 설정된 접두사를 봅니다.")
)
);
// execute 함수에서는 interaction.options.getSubcommandGroup() 와 interaction.options.getSubcommand() 를 함께 사용합니다.
서브커맨드 그룹은 명령어가 매우 복잡해질 때 유용하지만, 너무 많은 단계를 거치면 사용자 경험이 나빠질 수 있으니 신중하게 사용하는 것이 좋습니다.
명령어 배포의 중요성: deploy-commands.ts
다시 보기
지난 시간에 잠깐 언급했듯이, 슬래시 명령어(옵션이나 서브커맨드를 포함하여)를 만들거나 수정했다면, 변경된 내용을 디스코드에 알려주어야 합니다. 이 역할을 하는 것이 src/deploy-commands.ts
스크립트입니다.
discordjs_typescript_boilerplate
에서는 봇이 시작될 때(Events.ClientReady
) 연결된 모든 서버에 대해 자동으로 명령어를 배포(갱신)합니다. 이는 개발 중에는 매우 편리합니다.
deploy-commands.ts
는 src/commands/index.ts
에서 가져온 모든 명령어들의 data
속성 (즉, SlashCommandBuilder
로 정의된 명령어 구조)을 JSON
형태로 변환하여 디스코드 API로 전송합니다. 디스코드는 이 정보를 바탕으로 사용자 인터페이스에 슬래시 명령어를 표시하고, 옵션 입력 필드를 제공하며, 서브커맨드 선택지를 보여줍니다.
명령어 업데이트 시 주의사항:
- 명령어의 이름, 설명, 옵션, 서브커맨드 구조 등을 변경했다면, 봇을 재시작하여
deploy-commands
로직이 다시 실행되도록 해야 합니다. - 때로는 디스코드 클라이언트 자체에 명령어 정보가 캐시되어 즉시 반영되지 않는 것처럼 보일 수 있습니다. 이 경우, 몇 분 정도 기다리거나, 디스코드 클라이언트를 완전히 재시작(Ctrl/Cmd + R)해보는 것이 좋습니다.
- 글로벌 명령어 vs 서버(Guild) 명령어:
deploy-commands.ts
에서Routes.applicationGuildCommands(clientId, guildId)
를 사용하면 특정 서버에만 명령어를 등록합니다. 개발 및 테스트 시에는 이 방식이 빠르고 편리합니다. 모든 서버에 명령어를 등록하려면Routes.applicationCommands(clientId)
를 사용하며, 이를 '글로벌 명령어'라고 합니다. 글로벌 명령어는 모든 서버에 적용되지만, 전파되는 데 최대 1시간까지 걸릴 수 있습니다. Boilerplate는 각 서버에 개별적으로 등록하는 방식을 사용하고 있습니다.
마무리
이번 시간에는 슬래시 명령어에 옵션을 추가하여 사용자로부터 다양한 입력을 받고, 서브커맨드를 사용하여 관련된 기능들을 하나의 명령어로 묶는 방법을 배웠습니다. 이를 통해 훨씬 더 유연하고 강력한 명령어를 설계할 수 있게 되었습니다.
다음 시간에는 명령어 사용에 제한을 두는 '쿨타임' 기능과, 명령어 실행 중 발생할 수 있는 다양한 '오류'들을 어떻게 효과적으로 처리하고 사용자에게 피드백을 줄 수 있는지에 대해 알아보겠습니다. 안정적인 봇 운영을 위한 필수적인 내용이니 기대해주세요!
'DiscordJS 개발 튜토리얼' 카테고리의 다른 글
[DiscordJS 봇 개발 튜토리얼] 4. 명령어 쿨타임과 안정적인 오류 처리 (0) | 2025.06.05 |
---|---|
[DiscordJS 봇 개발 튜토리얼] 2. 명령어 구조 만들기: 슬래시 명령어를 위한 첫걸음 (1) | 2025.06.03 |
[DiscordJS 봇 개발 튜토리얼] 1. 봇 상태 메시지 설정하기 (2) | 2025.06.02 |
[DiscordJS 봇 개발 튜토리얼] 0. 프로젝트, 봇 생성하기 (3) | 2025.06.01 |