From a4f779b9a701f24aca194668a2c89909f5e6c98c Mon Sep 17 00:00:00 2001 From: sunwoo-j Date: Thu, 27 Jun 2024 17:10:27 +0900 Subject: [PATCH] Edited posts --- _posts/2024-05-22-discord-bot-1.md | 10 ++--- _posts/2024-05-24-discord-bot-2.md | 28 ++++++------ _posts/2024-05-27-discord-bot-3.md | 54 +++++++++++------------ _posts/2024-05-30-discord-bot-4.md | 56 +++++++++++------------ _posts/2024-06-01-discord-bot-6.md | 56 +++++++++++------------ _posts/2024-06-24-discord-bot-5.md | 71 +++++++++++++++--------------- 6 files changed, 138 insertions(+), 137 deletions(-) diff --git a/_posts/2024-05-22-discord-bot-1.md b/_posts/2024-05-22-discord-bot-1.md index 1386e495aad..cdb58e94b93 100644 --- a/_posts/2024-05-22-discord-bot-1.md +++ b/_posts/2024-05-22-discord-bot-1.md @@ -23,7 +23,7 @@ description: 디스코드 봇을 생성하고 채팅에 대답하게 하기 ![](/assets/img/discord%20bot/1_0.png) -완료되면 아래처럼 애플리케이션(Application)을 생성할 수 있는 개발자 포털 홈페이지가 나온다. 애플리케이션을 생성함으로써 인증 토큰을 제공받고 권한을 설정하는 등 디스코드 API와 상호작용을 할 수 있게 된다. +완료되면 아래처럼 애플리케이션(Application)을 생성할 수 있는 개발자 포털 홈페이지가 나온다. 애플리케이션을 생성함으로써 인증 토큰을 제공받고 권한을 설정하는 등 디스코드 API와 상호 작용을 할 수 있게 된다. ![](/assets/img/discord bot/1_1.png) @@ -37,7 +37,7 @@ description: 디스코드 봇을 생성하고 채팅에 대답하게 하기 이제 생성된 애플리케이션에 대한 정보가 화면에 뜬다. 여기서 아이콘, 이름과 설명 등을 수정할 수 있지만 애플리케이션 정보는 봇을 이용하는 사용자에게 직접적으로 보이지는 않는다. -> 봇 외에도 디스코드 API와 상호작용을 하는 모든 프로그램은 [디스코드 애플리케이션](https://discord.com/developers/docs/game-sdk/applications)이 필요하다. +> 봇 외에도 디스코드 API와 상호 작용을 하는 모든 프로그램은 [디스코드 애플리케이션](https://discord.com/developers/docs/game-sdk/applications)이 필요하다. {: .prompt-info } 우리는 봇을 만들 것이기 때문에 좌측 메뉴에서 Bot 탭을 눌러 이동해 주자. @@ -121,9 +121,9 @@ $ pip install discord.py ### 2. 디스코드와 봇 연결시키기 -명령어를 받고 답을 하려면 먼저 디스코드와 봇이 서로 통신이 되어야 한다. `discord.py`에는 통신을 위한 몇 가지 방법이 존재하는데 그중에서 우선 [**Client**](https://discordpy.readthedocs.io/en/stable/api.html#clients)에 대해 다뤄볼 것이다. +명령어를 받고 답을 하려면 먼저 디스코드와 봇이 서로 통신이 되어야 한다. `discord.py`에는 통신을 위한 몇 가지 방법이 존재하는데 그중에서 우선 [`Client`](https://discordpy.readthedocs.io/en/stable/api.html#clients)에 대해 다뤄볼 것이다. -`discord.Client`는 디스코드와의 연결을 나타내는 기본적인 객체라고 보면 되는데, 이를 활용해 디스코드 웹소켓과 API와의 상호작용이 가능하다. 아래는 `discord.py` documentation에서 제공하는 [간단한 시작 코드](https://discordpy.readthedocs.io/en/latest/quickstart.html#a-minimal-bot)다. 이번 글에서는 이 코드를 활용해 볼 것이다. +`discord.Client`는 디스코드와의 연결을 나타내는 기본적인 객체라고 보면 되는데, 이를 활용해 디스코드 웹소켓과 API와의 상호 작용이 가능하다. 아래는 `discord.py` documentation에서 제공하는 [간단한 시작 코드](https://discordpy.readthedocs.io/en/latest/quickstart.html#a-minimal-bot)다. 이번 글에서는 이 코드를 활용해 볼 것이다. ```python # This example requires the 'message_content' intent. @@ -150,7 +150,7 @@ async def on_message(message): client.run('your token here') ``` -코드를 보면 `client`를 먼저 설정하고 `on_ready()`와 `on_message()`라는 **이벤트(Event)**들에 대한 Event Handler를 지정한 것을 볼 수 있다. 여기서 말하는 이벤트는 메시지 전송, 사용자 밴과 채널 생성처럼 상태에 변화가 생겼을 때 디스코드가 호출하는 일종의 함수라고 이해하면 된다. `on_ready()`는 그중에서 `Client`가 디스코드와의 연결을 성공했을 때 보내지는 이벤트로, 봇이 사용자와 상호작용을 할 준비가 되었을 때 호출된다고 보면 된다. +코드를 보면 `client`를 먼저 설정하고 `on_ready()`와 `on_message()`라는 **이벤트(Event)**들에 대한 Event Handler를 지정한 것을 볼 수 있다. 여기서 말하는 이벤트는 메시지 전송, 사용자 밴과 채널 생성처럼 상태에 변화가 생겼을 때 디스코드가 호출하는 일종의 함수라고 이해하면 된다. `on_ready()`는 그중에서 `Client`가 디스코드와의 연결을 성공했을 때 보내지는 이벤트로, 봇이 사용자와 상호 작용을 할 준비가 되었을 때 호출된다고 보면 된다. `on_message()`는 봇이 속한 길드에서 메시지가 전송됐을 때 발동되는 이벤트이다. diff --git a/_posts/2024-05-24-discord-bot-2.md b/_posts/2024-05-24-discord-bot-2.md index 944b993da53..d82728021c9 100644 --- a/_posts/2024-05-24-discord-bot-2.md +++ b/_posts/2024-05-24-discord-bot-2.md @@ -1,7 +1,7 @@ --- title: 디스코드 봇 DIY - 2. 기본적인 명령어 설정 date: 2024-05-24 11:07:32 +/-TTTT -last_modified_at: 2024-05-24 22:57:40 +/-TTTT +last_modified_at: 2024-06-27 13:34:40 +/-TTTT categories: [Python, discord.py] tags: [python, discord, bot] description: discord.py 확장 라이브러리로 명령어 설정하기 @@ -14,7 +14,7 @@ description: discord.py 확장 라이브러리로 명령어 설정하기 ## Bot과 확장 라이브러리 -`Bot`은 앞서 본 `Client`의 기능들을 상속받는 일종의 subclass이다. `Client`만 활용해서 봇을 개발할 수도 있겠지만, `Client`는 이벤트를 일일이 지정해 줘야 하는 번거로움이 있다. 이와 같은 단점들을 보완하기 위해 `discord.py`는 확장 라이브러리인 `discord.ext`(extension)을 포함하고 있다. 이 라이브러리에 `Bot`이 포함되어 있는데, 편리성을 위해 만들어진 만큼 비교적 간편하게 명령어를 추가할 수 있다. +[`Bot`](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html?#bot)은 앞서 본 `Client`의 기능들을 상속받는 일종의 subclass이다. `Client`만 활용해서 봇을 개발할 수도 있겠지만, `Client`는 이벤트를 일일이 지정해 줘야 하는 번거로움이 있다. 이와 같은 단점들을 보완하기 위해 `discord.py`는 확장 라이브러리인 `discord.ext`(extension)를 포함하고 있다. 이 라이브러리에 `Bot`이 포함되어 있는데, 편리성을 위해 만들어진 만큼 비교적 간편하게 명령어를 추가할 수 있다. ### 1. Bot으로 명령어 추가하기 @@ -24,12 +24,12 @@ from discord.ext import commands bot = commands.Bot(command_prefix='!') # 명령어 인식 기호 ``` -`Bot`은 `discord.ext.commands`에 들어있기 때문에 `import discord`와 별개로 라이브러리를 추가해 주어야 한다. 그리고 `Bot`을 initialize할 때는 `command_prefix`를 지정해야 하는데, 여기서 지정된 string으로 input이 시작해야 명령어로 인식된다. +`Bot`은 확장 라이브러리에 들어있기 때문에 `import discord`와 별개로 라이브러리를 추가해 주어야 한다. 그리고 `Bot`을 초기화할 때는 `command_prefix`를 지정해야 하는데, 메시지가 여기서 **지정된 string으로 시작**해야 명령어로 인식된다. 이 경우에는 `'!'`로 설정되었으므로 느낌표로 시작하는 모든 메시지가 명령어로 인식이 된다. > `command_prefix`를 여러 개 지정하고 싶다면 `('!', '?')`처럼 지정할 수 있다. 빈 string을 지정할 수도 있는데, 이 경우에는 모든 입력이 명령어로 인식된다. {: .prompt-info} -`Command`는 명령어로 발동시키는 함수를 나타내는 객체다. 일반 함수 앞에 `@discord.ext.commands.command` decorator를 붙이면 `Command`가 되는데, 명령어가 decorator에서 정한 `name`과 일치한다면 `Command`가 발동한다. 예시로 전에 쓴 코드를 이를 활용해 바꿔 보겠다. +`Command`는 명령어로 발동시키는 함수를 나타내는 class다. 일반 함수 앞에 `@bot.command` decorator를 붙이면 `Command` 객체가 되는데, `command_prefix` 뒤에 붙은 명령어가 decorator에서 정한 `name`과 일치한다면 `Command`가 발동한다. 예시로 이전에 쓴 코드를 `@bot.command`를 활용해 바꿔 보겠다. ```python # bot.py @@ -84,14 +84,14 @@ async def hello(ctx): bot.run(TOKEN) ``` -두 코드 모두 동일하게 작동한다. 자세히 들여다보면 `on_message()` 대신에 `Command`를 활용한 것을 볼 수 있다. `Client`를 사용할 때는 텍스트로 함수를 호출하기 위해서 `on_message()`라는 이벤트를 특정해 줘야 했지만, `Command`를 활용하면 이벤트를 따로 설정할 필요 없이 바로 명령어가 인식되는 모습이다. Decorator에서 `name='hello'`로 argument를 지정했기 때문에 `command_prefix`와 `name`이 합쳐진 `$hello`를 입력하면 설정된 함수가 호출된다. +두 코드 모두 동일하게 작동한다. 자세히 들여다보면 `on_message()` 대신에 `Command`를 활용한 것을 볼 수 있다. `Client`를 사용할 때는 텍스트로 함수를 호출하기 위해서 `on_message()`라는 이벤트를 특정해 줘야 했지만, `Command`를 활용하면 이벤트를 따로 설정할 필요 없이 메시지에서 바로 명령어가 인식되는 모습이다. Decorator에서 `name='hello'`로 argument를 지정했기 때문에 `command_prefix`와 `name`이 합쳐진 `$hello`를 입력하면 설정된 함수가 호출된다. -> **`ctx`**는 명령어를 실행한 사람이 누군지와 어떤 채널에서 입력됐는지 등의 [context 정보](https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#invocation-context)를 담고 있다. +> **`ctx`**는 명령어를 실행한 사람이 누군지와 어떤 채널에서 입력됐는지 등의 [**Context 정보**](https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html#invocation-context)를 담고 있다. {: .prompt-info} ![](/assets/img/discord%20bot/2_1.png) -`Command`를 쓰면 또 좋은 점이 함수들 중 명령어에 해당하는 것이 무엇인지 인지하고 있으며, 따로 추가하지 않아도 `help` 명령어로 봇에 설정된 명령어들의 목록을 보는 것이 가능하다는 점이다. +`Command`를 쓰면 또 좋은 점이 내가 쓴 함수 중 명령어에 해당하는 것이 무엇인지 인지하고 있으며, 따로 추가하지 않아도 `help` 명령어로 봇에 설정된 명령어들의 목록을 보는 것이 가능하다는 점이다. ```python @bot.command(name='hello', help="인사를 합니다") @@ -103,7 +103,7 @@ bot.run(TOKEN) ### 2. 명령어에 Parameter 추가하기 -`Client`로 명령을 받을 때는 전부 string으로 받기 때문에 함수 안에서 type 변환을 해야 했지만, `Command`를 활용하면 parameter에서 원하는 type을 미리 설정할 수 있다. 아래는 간단한 예시다. +`Client`로 명령을 받을 때는 전부 string으로 받기 때문에 함수 안에서 type 변환을 해야 했지만, `Command`를 활용하면 parameter에서 **원하는 type**을 미리 설정할 수 있다. 아래는 간단한 예시다. ```python @bot.command(name='곱하기', help="숫자 두 개를 곱합니다") @@ -112,7 +112,7 @@ async def multiply(ctx, first_int: int, second_int: int): await ctx.send("결과는 ", product, "입니다.") ``` -`first_int`와 `second_int`에 `: int`를 붙여서 이 parameter들은 정숫값을 받는다고 설정해둔 것이다. 다만 정수가 아닌 값을 넣지 못하게 막아둘 수는 없어서 error handling은 따로 해야 한다. 더 나아가 parameter에는 단순히 string이나 int 같은 기본적인 type 말고도 길드나 멤버 같은 디스코드 API만의 특수한 type도 들어갈 수 있다. +`first_int`와 `second_int`에 `: int`를 붙여서 이 parameter들은 정숫값을 받는다고 설정해 두었다. 다만 정수가 아닌 값을 넣지 못하게 막아둘 수는 없어서 error handling은 따로 해야 한다. 더 나아가 parameter에는 단순히 string이나 int 같은 기본적인 type 말고도 길드나 멤버 같은 디스코드 API만의 특수한 type도 들어갈 수 있다. ```python @bot.command(name='참가일', help="멤버의 서버 참가 날짜를 알려줍니다") @@ -121,11 +121,11 @@ async def joined(ctx, member: discord.Member): await ctx.send(f"{member.display_name}님은 {join_date}에 서버에 참가했습니다.") ``` -멤버가 길드에 언제 참여했는지를 보여주는 명령어를 만들었다. `discord.Member`를 type으로 설정함으로써 길드에 참여하고 있는 사용자 객체를 받아들이는 변수로 지정했다. 그렇기에 `Member`의 attribute 중 하나인 `joined_at`을 받아와 출력할 수 있는 것이다. +멤버가 길드에 언제 참여했는지를 보여주는 명령어를 만들었다. `discord.Member`를 type으로 설정함으로써 길드에 참여하고 있는 사용자 객체를 받아들이는 변수로 지정했다. 그렇기에 `Member`의 attribute 중 하나인 `joined_at`을 받아와 출력할 수 있다. 2_3 -이런 식으로 `Member`가 현재 길드에 언제 참가했는지 보여줄 수 있다. 참고로 `Member.display_name`은 채팅 등 디스코드 UI에 보이는 별명, `Member.name`은 사용자명, `Member.id`는 사용자 고유 ID니 적재적소에 활용할 수 있도록 하자. +이런 식으로 `Member`가 현재 길드에 언제 참가했는지 보여준다. 참고로 `Member.display_name`은 채팅 등 디스코드 인터페이스에 보이는 길드 내 별명, `Member.name`은 사용자명, `Member.id`는 변하지 않는 사용자 고유 ID니, 적재적소에 활용할 수 있도록 하자. ### 3. 명령어 Exception 관리하기 @@ -136,16 +136,16 @@ async def multiply_error(ctx, error): await ctx.send("오류: 정수 두 개를 입력해 주세요.") ``` -확장 라이브러리의 장점이 여기 또 나온다. `Command`의 **exception**을 넘겨받아 따로 error handling을 할 수 있는 건데, 위의 경우 argument가 정해진 type과 다르게 주어질 때 발생하는 `BadArgument`를 처리하고 있다. +확장 라이브러리의 장점이 여기 또 있다. `Command`의 **exception을 넘겨받아** 따로 error handling을 할 수 있는 건데, 위의 경우 argument가 정해진 type과 다르게 주어질 때 발생하는 `BadArgument`를 처리하고 있다. > `Commands`에서 발생할 수 있는 exception 목록은 [여기](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html?error#exceptions)서 확인할 수 있다. {: .prompt-info} ![](/assets/img/discord%20bot/2_4.png) | ![](/assets/img/discord%20bot/2_5.png) -정상적으로 int type이 주어졌을 때와 그렇지 않을 때 각자 다른 응답을 하는 것을 볼 수 있다. +정상적으로 정수가 주어졌을 때와 그렇지 않아 exception이 발생했을 때 각각 다른 응답을 하는 것을 볼 수 있다. -이외에도 parameter마다 설명을 추가하거나 생성된 리스트에서 변수를 정하는 식으로 오류를 원천 차단 하는 방법이 있지만 나중에 다른 명령어 시스템을 다룰 때 정리해 보겠다. +이외에도 parameter마다 설명을 추가하거나 생성된 리스트에서 변수를 정하는 식으로 오류를 원천 차단 하는 방법이 있지만 다음에 다른 명령어 시스템을 다룰 때 정리해 보겠다. ## 부록 diff --git a/_posts/2024-05-27-discord-bot-3.md b/_posts/2024-05-27-discord-bot-3.md index 53eff885595..04a316a90ad 100644 --- a/_posts/2024-05-27-discord-bot-3.md +++ b/_posts/2024-05-27-discord-bot-3.md @@ -1,7 +1,7 @@ --- title: 디스코드 봇 DIY - 3. 신규 멤버 환영 메시지 date: 2024-05-27 10:53:19 +/-TTTT -last_modified_at: 2024-05-29 18:59:33 +/-TTTT +last_modified_at: 2024-06-27 14:59:33 +/-TTTT categories: [Python, discord.py] tags: [python, discord, bot] description: 디스코드 이벤트 활용해서 환영 메시지 보내기 @@ -14,14 +14,14 @@ description: 디스코드 이벤트 활용해서 환영 메시지 보내기 ## 이벤트 카테고리 -여태까지 글에서 다룬 이벤트는 `on_ready()`와 `on_message()` 밖에 없지만 디스코드에는 이벤트로 치는 특수한 상황들이 이보다 훨씬 많이 있다. 이벤트의 종류에는 무엇이 있고 봇이 그것들을 어떻게 활용할 수 있을지 알아보자. +여태까지 글에서 다룬 이벤트는 `on_ready()`와 `on_message()`밖에 없지만 디스코드에는 이벤트로 치는 특수한 상황들이 이보다 훨씬 많이 있다. 이벤트의 종류에는 무엇이 있고 봇이 그것들을 어떻게 활용할 수 있을지 알아보자. > 이벤트에 대응하는 함수는 앞에 `async def`를 붙여 coroutine으로 정의되어야 한다. {: .prompt-warning} -아래는 `discord.py`에서 나눠놓은 이벤트 카테고리들을 표로 만들어 가져왔다. [**Event Reference** 페이지](https://discordpy.readthedocs.io/en/latest/api.html?#discord-api-events)에서 본인이 만들고자 하는 기능에 맞는 카테고리로 이동해 해당하는 이벤트를 찾으면 수월한 개발이 가능하다. Intents를 `discord.Intents.all()`로 설정하지 않고 필요한 것만 따로 설정할 생각이라면 카테고리별 intents 필요사항을 확인하면 된다. +아래는 `discord.py`에서 나눠놓은 이벤트 카테고리들을 표로 만들어 가져왔다. [**Event Reference** 페이지](https://discordpy.readthedocs.io/en/latest/api.html?#discord-api-events)에서 본인이 만들고자 하는 기능에 맞는 카테고리로 이동해 해당하는 이벤트를 찾으면 수월한 개발이 가능하다. 인텐트를 `discord.Intents.all()`로 설정하지 않고 필요한 것만 따로 설정할 생각이라면 카테고리별 인텐트 필요사항을 확인하면 된다. -| 카테고리 | 설명 | Intents 추가 필요사항 | +| 카테고리 | 설명 | 인텐트 추가 필요사항 | | :--------------- | :--------------------------- | :------------------------------------------------------- | | App commands | 기본적인 앱 기능 관련 | | | AutoMod | AutoMod(자동 검열 기능) 관련 | `auto_moderation_configuration` | @@ -32,7 +32,7 @@ description: 디스코드 이벤트 활용해서 환영 메시지 보내기 | Gateway | 클라이언트의 연결 완성 여부 | | | Guilds | 특정 길드 내 이벤트 관련 | `guilds`, `emojis_and_stickers`, `moderation`, `invites` | | Integration | 타 서비스와의 연동 관련 | `integrations`, `webhooks` | -| Interactions | 상호작용 발생 시 | | +| Interactions | 상호 작용 발생 시 | | | Members | 길드 내 멤버 관련 | `members`, `moderation`, `presences` | | Message | 메시지 전송 관련 | `messages` | | Polls | 투표 관련 | `message_content`, `polls` | @@ -45,17 +45,17 @@ description: 디스코드 이벤트 활용해서 환영 메시지 보내기 이번 글에서는 새로운 멤버가 길드에 참가할 때 여러 가지를 시도해 보려 한다. 그러니 멤버들과 관련된 카테고리인 **Members**에서 길드 참가와 관련된 이벤트를 확인하면 된다. `discord.py` API 문서를 확인해 보니 `on_member_join()`이 내가 찾는 이벤트인 것을 알 수 있었다. -## 신규 유저 관리하기 +## 신규 사용자 관리하기 -길드에 새로운 멤버가 참가할 때는 이 멤버가 길드에 잘 녹아들게 하면서도 취지에 맞지 않는 행동을 막는 것이 중요하다. 공지를 읽게 하면 되겠지만 기본적으로 공지를 띄워도 강제로 읽게 설정하는 기능은 없다. **커뮤니티 길드** 같은 경우에는 규칙에 동의해야 길드 이용이 가능한 *규칙 심사* 기능이 있긴 하지만, 커뮤니티 활성화를 하지 않으면 사용할 수 없다. +길드에 새로운 멤버가 참가할 때는 이 멤버가 길드에 잘 녹아들게 하면서도 **취지에 맞지 않는 행동**을 막는 것이 중요하다. 공지를 읽게 하면 되겠지만 기본적으로 공지를 띄워도 강제로 읽게 설정하는 기능은 없다. **커뮤니티 길드** 같은 경우에는 규칙에 동의해야 길드 이용이 가능한 **규칙 심사 기능**이 있긴 하지만, 커뮤니티 활성화를 하지 않으면 사용할 수 없다. -꼭 규칙이 아니더라도 기본적인 정보들을 전달해 주면 참여도가 높아질 가능성이 커진다. 기존 유저들이 있는 상황에서 아무것도 모르는 상태로 길드에 참여해서 활동하는 건 어지간히 어렵기 때문이다. 그래서 신규 멤버가 들어올 때 규칙과 정보를 전달할 수 있도록 봇을 설정해두는 길드들이 많이 있다. +꼭 규칙이 아니더라도 **기본적인 정보들**을 전달해 주면 참여도가 높아질 가능성이 커진다. 기존 사용자들이 있는 상황에서 아무것도 모르는 상태로 길드에 참여해서 활동하는 건 어지간히 어렵기 때문이다. 그래서 신규 멤버가 들어올 때 규칙과 정보를 전달할 수 있도록 봇을 설정해 두는 길드들이 많이 있다. -당장은 강제로 공지를 읽게 하거나 들어올 때 역할을 부여하는 **Reaction Roles** 같은 기능을 만들진 않겠지만, 추후에 확장할 수 있도록 신규 멤버 참가 시에 메시지를 보내는 기능을 만들어 보려 한다. +공지를 읽어야 길드 내 권한이 주어지도록 역할을 자동으로 부여하는 **Reaction Roles** 같은 기능은 다음에 확장할 수 있도록 하고, 우선 신규 멤버 참가 시에 메시지를 보내는 기능을 만들어 보려 한다. ### 1. 길드 참가 환영 DM 보내기 -디스코드에도 자체적으로 길드에 참가할 때 보내지는 메시지가 있다. 하지만 원하는 메시지를 보내거나, 꼭 메시지가 아니더라도 Reaction Roles 같은 절차를 밟게 만들려면 위에서 언급한 `on_member_join()` 이벤트를 활용하면 된다. +디스코드에도 자체적으로 길드에 참가할 때 보내지는 알림 메시지가 있다. 하지만 원하는 메시지를 보내거나, 꼭 메시지가 아니더라도 Reaction Roles 같은 절차를 밟게 만들려면 위에서 언급한 `on_member_join()` 이벤트를 활용하면 된다. ```python @bot.event @@ -63,30 +63,30 @@ async def on_member_join(member): await member.send(f"{member.name}님, 반갑습니다.") ``` -`on_member_join()`에는 들어온 멤버가 parameter로 설정되어 있기 때문에 받아온 `member` 객체를 활용해 호출될 함수를 짜주면 된다. `member.send()`를 통해 정해진 메시지가 참가한 멤버에게 DM으로 보내지게 된다. +`on_member_join()`에는 들어온 멤버가 parameter로 설정되어 있기 때문에 받아온 `member` 객체를 활용해 호출될 함수를 짜주면 된다. `member.send()`를 통해 작성한 메시지가 참가한 멤버에게 DM으로 보내지게 된다. -> `discord.Intents.default()`로 해놨다면 `intents.members = True`를 따로 추가해 주어야 한다. +> 인텐트 설정을 **all로 설정**하지 않았다면 `intents.members = True`를 따로 추가해 주어야 한다. {: .prompt-warning} ![](/assets/img/discord%20bot/3_1.png) - 깡통 계정으로 서버에 참가하자마자 봇에게 DM을 받은 모습이다. 지금은 단순한 메시지만 한 줄 보냈지만 서버 규정이나 안내 같은 중요한 내용을 따로 보낼 수도 있겠다. 다만 DM으로 보낼 때의 단점이 명확한데, 만약 사용자가 **DM을 차단시켜놓았다면** 메시지가 제대로 전달되지 않는다. 때문에 DM보다 길드 내의 채널을 활용하는 편이 나을 수 있다. + 깡통 계정으로 길드에 참가하자마자 봇에게 DM을 받은 모습이다. 지금은 단순한 메시지만 한 줄 보냈지만, 길드 규정이나 안내 같은 중요한 내용을 따로 보낼 수도 있겠다. 다만 DM으로 보낼 때의 단점이 명확한데, 만약 사용자가 **DM을 차단해 놓았다면** 메시지가 제대로 전달되지 않는다. 그 때문에 DM보다 **길드 내의 채널**을 활용하는 편이 나을 수 있다. ### 2. 길드 채널에 환영 메시지 보내기 -DM을 제외하면 글을 보낼 수 있는 남은 공간은 길드의 채팅 채널이다. 지난번에 명령어를 다룰 때는 `ctx`로 명령어를 입력한 채널의 정보를 불러와 다시 메시지를 보낼 수 있었지만, `on_member_join()`에는 채널 정보 없이 `member`만 주어진다. 그렇기 때문에 채널에 메시지를 보내려면 직접 채널 ID를 가져와야 한다. +DM을 제외하면 글을 보낼 수 있는 공간은 길드의 채팅 채널이다. 지난번에 명령어를 다룰 때는 `ctx`로 명령어를 입력한 채널의 정보를 불러와 다시 메시지를 보낼 수 있었지만, `on_member_join()`에는 채널 정보 없이 `member`만 주어진다. 그렇기 때문에 채널에 메시지를 보내려면 직접 **채널 ID**를 가져와야 한다. ![](/assets/img/discord%20bot/3_2.png) -바로 확인 할 수는 없고 **개발자 모드**를 활성화해야 한다. 아래 톱니바퀴를 눌러 사용자 설정에 들어가자. +바로 확인할 수는 없고 **개발자 모드**를 활성화해야 한다. 아래 톱니바퀴를 눌러 사용자 설정에 들어가자. ![](/assets/img/discord%20bot/3_3.png) -왼쪽 목록에서 앱 설정 아래 있는 고급에 들어가서 첫 번째 항목인 개발자 모드를 활성화하면 된다. +왼쪽 목록에서 앱 설정 아래 있는 고급에 들어가서 첫 번째 항목인 **개발자 모드**를 활성화하면 된다. ![](/assets/img/discord%20bot/3_4.png) -이제 서버로 돌아가 봇이 채팅을 보냈으면 하는 채팅 채널을 우클릭하면 채널 ID 복사하기가 뜰 것이다. ID를 복사했으면 이제 코드에 써먹어보자. +이제 길드로 돌아가 봇이 채팅을 보냈으면 하는 채팅 채널을 우클릭하면 채널 ID 복사하기가 나타날 것이다. ID를 복사했으면 이제 코드에서 활용해 보자. ```python @bot.event @@ -99,21 +99,21 @@ async def on_member_join(member): 3_5 -깡통 계정으로 다시 서버에 들어오니 복사한 ID의 채널에 메시지를 보내는 모습을 확인할 수 있다. 지금은 채널이 한 개 밖에 없어서 일반 채널의 ID를 그대로 붙여넣기했지만 채널이 여러 개라면 관리자가 원하는 채널에 보낼 수 있도록 변수 처리하는 편이 나을 것이다. +깡통 계정으로 다시 서버에 들어오니 복사한 ID의 채널에 메시지를 보내는 모습을 확인할 수 있다. 지금은 채널이 한 개밖에 없어서 일반 채널의 ID를 그대로 붙여넣기 했지만 채널이 여러 개라면 관리자가 원하는 채널에 보낼 수 있도록 **변수 처리**하는 편이 나을 것이다. -또 다른 문제도 있다. 지금은 테스트용 길드 하나만 두고 있지만 디스코드 봇을 개발하면 두 개 이상의 길드에 참가할 수 있다. 그렇다면 메시지를 보내도록 설정된 채널이 속한 길드가 아닌, **내 봇이 존재하는 다른 길드**에 참가하더라도 이 채널로 모든 환영 메시지가 모이게 된다. +또 다른 문제도 있다. 지금은 테스트용 길드 하나만 두고 있지만, 디스코드 봇을 개발하면 일반적으로 여러 개의 길드에 접속하게 된다. 그렇다면 메시지를 보내도록 설정된 채널이 속한 길드가 아닌, **내 봇이 존재하는 다른 길드**에 참가하더라도 이 채널로 모든 환영 메시지가 모이게 된다. 3_6 -*테스트 서버 2*라는 길드를 하나 더 파서 봇을 추가한 후에 깡통 계정을 초대해 보았다. 그래보니 새로 만든 길드의 일반 채널이 아니라, 위의 사진처럼 원래 있던 길드에 환영 메시지가 재출력되는 것을 확인할 수 있었다. 이제 이 문제를 어떻게 해결할지 알아보자. +**봇 테스트 서버**와 별도로 길드를 하나 더 생성해서 봇을 추가한 후에 깡통 계정을 초대해 보았다. 그러니 이미 멤버임에도 불구하고 원래 있던 길드에 환영 메시지가 재출력되는 것을 확인할 수 있었다. 이제 이 문제를 어떻게 해결할지 알아보자. ### 3. 길드 특정하기 -길드도 채널과 마찬가지로 고유 ID를 가지고 있다. 채널 ID를 확인하기 위해 개발자 옵션을 활성화했으니 마찬가지로 **길드 ID**도 디스코드 인터페이스에서 확인할 수 있다. +길드도 채널과 마찬가지로 고유 ID를 가지고 있다. 채널 ID를 확인하기 위해 개발자 옵션을 활성화했으니, 마찬가지로 **길드 ID**도 디스코드 인터페이스에서 확인할 수 있다. ![](/assets/img/discord%20bot/3_7.png) -좌측 상단의 길드 이름을 우클릭하고 서버 ID 복사하기를 눌러주면 복사가 된다. 원래 테스트하고 있던 서버에서 계속 테스트를 진행할 것이기 때문에 `.env` 파일에 `GUILD_ID`와 `CHANNEL_ID` 항목을 추가해서 복사한 고유 ID를 넣어줬다. +좌측 상단의 길드 이름을 우클릭하고 서버 ID 복사하기를 눌러주면 복사가 된다. 원래 테스트하고 있던 서버에서 계속 테스트를 진행할 것이기 때문에 `.env` 파일에 `GUILD_ID`와 `CHANNEL_ID` 항목을 추가해서 복사한 길드 고유 ID와 코드에 넣었던 채널 ID를 지정해 주었다. ```text # .env @@ -129,7 +129,7 @@ GUILD = int(os.getenv('GUILD_ID')) CHANNEL = int(os.getenv('CHANNEL_ID')) ``` -`.env`에서 받아올 때는 string으로 받아오기 때문에 원래 int type인 길드와 채널 ID를 다시 int로 변환해 주었다. +환경 변수를 받아올 때는 전부 string으로 받아오므로 원래 int type인 길드 ID와 채널 ID를 다시 int로 변환한다. ```python @bot.event @@ -141,13 +141,13 @@ async def on_ready(): ) ``` -하는 김에 `on_ready()` 이벤트에 길드 정보도 추가해 주기로 했다. `utils.find()`라는 `discord.py` 함수를 통해 `bot.guilds`, 즉 봇이 연결을 마친 길드들 중에서 `.env`에서 불러온 ID와 같은 ID를 가지고 있는 길드를 찾아낸다. 이렇게 디스코드 서버와 클라이언트의 연결을 확인하는 것을 넘어 테스트 중인 특정한 길드에 연결을 마쳤는지 확인이 가능하다. +하는 김에 `on_ready()` 이벤트에 길드 정보도 추가해 주기로 했다. `utils.find()`라는 `discord.py` 함수를 통해 `bot.guilds`, 즉 봇이 연결을 마친 길드 중에서 `.env`에서 불러온 ID와 같은 ID를 가지고 있는 길드를 찾아낸다. 이렇게 디스코드 서버와 클라이언트의 연결을 확인하는 것을 넘어 **테스트 중인 특정한 길드**에 연결을 마쳤는지 확인이 가능하다. ```python welcome_channel = {GUILD:CHANNEL} ``` -길드 채널들 중에 메시지를 보낼 채널을 설정하기 위해 `welcome_channel`이라는 dictionary를 만들어서 길드 ID를 key로 설정하고 그 길드에서 메시지를 보낼 채널 ID를 value로 넣었다. Dictionary로 만든 이유는 나중에 여러 길드에서 사용될 경우 추가하기 용이하도록 하기 위함이다. +길드 채널 중에 메시지를 보낼 채널을 설정하기 위해 `welcome_channel`이라는 dictionary를 만들어서 길드 ID를 key로 설정하고 그 길드에서 메시지를 보낼 채널 ID를 value로 넣었다. Dictionary로 만든 이유는 나중에 여러 길드에서 사용될 경우 값을 추가하기 용이하도록 하기 위함이다. ```python @bot.event @@ -158,9 +158,9 @@ async def on_member_join(member): await channel.send(f"{member.display_name}님이 서버에 참가하셨습니다.") ``` -`on_member_join()`의 argument로 받은 `member`에게서 막 참가한 길드의 ID를 따와 `welcome_channel`에서 상응하는 채널 ID를 받아내게 했으며, ID로 특정된 채널에 메시지를 출력하도록 설정했다. 이러면 길드마다 각각 설정된 개별 채널로 환영 메시지를 전송할 수 있다. +`on_member_join()`의 argument로 받은 `member`에서 막 참가한 길드의 ID를 따와 `welcome_channel`에서 상응하는 채널 ID를 받아내게 했으며, ID로 특정된 채널에 메시지를 출력하도록 설정했다. 이러면 길드마다 각자 설정된 개별 채널로 환영 메시지를 전송할 수 있다. -지금은 길드와 채널 ID를 직접 환경 변수로 적어 넣었지만, 길드 관리자가 환영 메시지를 보낼 채널을 봇에게 설정하도록 할 수 있겠다. 또, 길드마다 설정을 저장하려면 데이터베이스가 필요하기 때문에 나중에 다시 돌아오는 편이 나을 거라 생각된다. 다음은 본격적인 봇 개발을 시작하기 앞서 사용자가 봇과 어떻게 상호작용을 할 수 있는지 배워볼 것이다. +지금은 길드와 채널 ID를 직접 환경 변수로 파일에 적어 넣었지만, 길드 관리자가 환영 메시지를 보낼 채널을 명령어로 입력하여 봇에게 설정하도록 할 수 있겠다. 또, 길드마다 설정을 저장하려면 **데이터베이스**가 필요하기 때문에 나중에 다시 돌아와서 기능을 추가하는 편이 나을 거라 생각된다. 다음은 본격적인 봇 개발을 시작하기에 앞서 사용자가 봇과 어떻게 상호 작용을 할 수 있는지 배워볼 것이다. ## 부록 diff --git a/_posts/2024-05-30-discord-bot-4.md b/_posts/2024-05-30-discord-bot-4.md index beac2bda0ae..d3094068077 100644 --- a/_posts/2024-05-30-discord-bot-4.md +++ b/_posts/2024-05-30-discord-bot-4.md @@ -1,10 +1,10 @@ --- title: 디스코드 봇 DIY - 4. Application Command date: 2024-05-30 10:44:15 +/-TTTT -last_modified_at: 2024-05-31 11:02:25 +/-TTTT +last_modified_at: 2024-06-27 15:28:58 +/-TTTT categories: [Python, discord.py] tags: [python, discord, bot] -description: 내 디스코드 앱과 상호작용하는 기본적인 방법들 +description: 내 디스코드 앱과 상호 작용을 하는 기본적인 방법들 --- > 이 글에서 다루는 내용 @@ -14,29 +14,29 @@ description: 내 디스코드 앱과 상호작용하는 기본적인 방법들 ## Application Command -**Application Command**는 디스코드 인터페이스에서 앱과 상호작용할 수 있는 기본적인 방법들을 지칭한다. 디스코드에서는 세 가지로 Application Command를 구분 짓고 있다. +**Application Command**는 디스코드 인터페이스에서 앱과 상호 작용을 할 수 있는 정식적인 방법들을 지칭한다. 디스코드에서는 세 가지로 Application Command를 구분 짓고 있다. > Application Command에 대한 자세한 내용은 [이곳](https://discord.com/developers/docs/tutorials/upgrading-to-application-commands)에서 볼 수 있다. {: .prompt-info} ![](/assets/img/discord%20bot/4_1.png) -세 종류의 Application Command를 표현한 이미지다. 왼쪽부터 Slash Command, Message Command, 그리고 User Command를 표현했다. +세 종류의 Application Command를 표현한 이미지다. 왼쪽부터 **Slash Command**, **Message Command**, 그리고 **User Command**를 표현했다. - **Slash Command**
-가장 흔히 사용하는 Application Command로, 채팅창에 슬래시(/)를 이용해 명령어를 입력하거나 메시지 입력창 왼쪽의 + 버튼을 누르면 나오는 앱 사용으로 명령어를 선택할 수 있다. +가장 흔히 사용하는 Application Command로, 채팅창에 빗금(/)을 이용해 명령어를 입력하거나 메시지 입력창 왼쪽의 + 버튼을 누르면 나오는 앱 사용으로 명령어를 선택할 수 있다. - **Message Command**
-Mesage Command는 특정 메시지와 관련된 행동을 할 때 유용하다. 원하는 메시지를 우클릭하거나 메시지의 우측 상단 ... 버튼을 누르면 보이는 에서 행동을 선택할 수 있다. +Message Command는 특정 메시지와 관련된 행동을 할 때 유용하다. 원하는 메시지를 우클릭하거나 메시지의 우측 상단 ... 버튼을 누르면 보이는 에서 행동을 선택할 수 있다. - **User Command**
User Command는 특정 사용자와 관련된 행동을 할 때 유용하다. Message Command와 비슷하게 원하는 사용자의 아바타나 이름을 우클릭하면 나오는 에서 행동을 선택할 수 있다. -앞에서는 `discord.ext.commands.Bot`의 `command_prefix`로 본인이 원하는 명령어 인식 기호를 설정해 명령어를 입력하게 만들었지만, Slash Command를 사용한다면 모든 명령어 인식 기호가 슬래시로 통일된다. 대신에 디스코드 인터페이스에서 **자동완성 기능**이 지원되고 명령어나 paramter에 대한 설명도 별다른 설정 없이 보여줄 수 있다. +앞에서는 `discord.ext.commands.Bot`의 `command_prefix`로 본인이 원하는 명령어 인식 기호를 설정해 명령어를 입력하게 했지만, Slash Command를 사용한다면 모든 명령어 인식 기호가 **빗금으로 통일**된다. 대신에 디스코드 인터페이스에서 자동완성 기능이 지원되고 명령어나 paramter에 관한 설명도 별다른 설정 없이 보여줄 수 있다. ### 1. 기존 명령어 Slash Command로 바꾸기 -일반 명령어를 Application Command로 설정하는데 중요한 키워드는 `tree`다. 지금까지는 decorator로 `@bot.command`를 썼었지만, 디스코드에서 정식으로 인지하는 명령어를 쓰기 위해서는 `@bot.tree.command`로 바꿔줘야 한다. `Bot.tree`는 `CommandTree`의 일종으로 **Application Command들을 저장하는 저장소** 개념이라고 보면 된다. 그래서 `command`를 `@bot.tree`에 연결하면 해당 명령어가 봇의 저장소에 저장된다. +일반 명령어를 Application Command로 설정하는 데 중요한 키워드는 `tree`다. 지금까지는 `@bot.command`를 썼었지만, 디스코드에서 정식으로 인지하는 Slash Command로 전환하기 위해서는 decorator를 `@bot.tree.command`로 바꿔야 한다. `Bot.tree`는 `CommandTree`의 일종으로 **명령어들을 저장하는 저장소** 개념이라고 보면 된다. 그래서 `@bot.tree.command`로 명령어를 생성하면 해당 명령어가 봇의 저장소에 자동으로 저장된다. ```python # 수정 전 @@ -52,11 +52,11 @@ async def hello(interaction: discord.Interaction): await interaction.response.send_message("Hello!") ``` -일전에 만들었던 `hello` 명령어에 `tree`를 넣어 Application Command로 만들었다. 다른 점으로는 `help` parameter가 `description`으로 이름이 바뀐다는 점과 `ctx` 대신 `interaction`을 받아와 사용한다는 점이 있다. +일전에 만들었던 `hello` 명령어에 `tree`를 넣어 Slash Command로 만들었다. 다른 점으로는 `help` parameter가 `description`으로 이름이 바뀐다는 점과 `ctx` 대신 `interaction`을 받아와 사용한다는 점이 있다. 4_2 -위에 보이는 것처럼 /he만 쳐도 /hello 명령어 자동완성 옵션이 뜬다. 이런 식으로 명령어 자동완성이 지원되며 명령어 설명 또한 직관적으로 명령어 아래 적혀있다. Parameter가 설정된 명령어도 Application Command를 사용하면 더 직관적으로 바뀐다. +위에 보이는 것처럼 앞 글자만 입력해도 **/hello** 명령어를 고르는 선택지가 보인다. 이런 식으로 명령어 **자동완성**이 지원되며 명령어 설명 또한 직관적으로 명령어 아래 적혀있다. Parameter가 설정된 명령어도 Slash Command를 사용하면 더 직관적으로 바뀐다. ```python # 수정 전 @@ -77,15 +77,15 @@ async def joined(interaction: discord.Interaction, member: discord.Member): await interaction.response.send_message(f"{member.display_name}님은 {join_date}에 서버에 참가했습니다.") ``` -`app_commands` 모듈을 불러오면 `@app_commands.describe()`라는 decorator를 사용할 수 있다. 이 안에는 명령어가 가진 parameter에 대한 설명을 추가할 수 있는데, 서버에 참가한 날짜를 조회할 `member` parameter에 대해 설명을 달아주었다. +`app_commands` 모듈을 불러오면 `@app_commands.describe()`라는 decorator를 사용할 수 있다. 이 안에서 명령어에서 설정된 **parameter에 대한 설명**을 추가할 수 있는데, 위에서는 `member`라는 parameter에 관해 설명을 달아 주었다. 4_3 -Parameter에 대한 설명이 뜨는 것은 물론이고, `Member` 객체를 data type으로 지정하니 길드에 있는 멤버들 목록이 자동으로 보인다. 수정 전에 사용자명을 직접 타이핑했던 것과 비교하면 굉장히 편리하다. 더불어 잘못된 값을 넣지 못하게 **알아서 exception handling을 해준다**. +Parameter에 대한 설명을 알려주는 것은 물론이고, `discord.Member` 객체를 type으로 지정하니 길드에 있는 멤버들 목록을 자동으로 띄워준다. 수정 전에 사용자명을 직접 한 글자씩 입력했던 것과 비교하면 굉장히 편리하다. 더불어 잘못된 값을 넣지 못하게 알아서 **exception handling**을 해준다. ### 2. 명령어 Sync 해주기 -이제 Application Command를 활용할 수 있게 되었으니 여러 가지 명령어들을 신나게 추가할 시간이다. 하지만 명령어를 추가하거나 수정했는데 아무리 새로고침을 해도 디스코드 인터페이스에는 명령어가 보이지 않거나 수정이 적용되지 않은 것을 관찰할 수 있다. 명령어를 따로따로 설정해 둔 이전과 다르게 담아놓은 `CommandTree`가 **동기화가 되지 않은 것**인데, `tree`에 있는 Application Command들은 `sync()`를 돌려주어야지 **디스코드 인터페이스에 동기화가 된다**. +이제 Slash Command를 활용할 수 있게 되었으니 여러 가지 명령어들을 신나게 추가할 시간이다. 하지만 명령어를 추가했는데 아무리 새로고침을 해도 디스코드 인터페이스에는 명령어가 보이지 않는 것을 확인할 수 있다. 명령어를 따로따로 설정해 둔 이전과 다르게 Application Command들을 모아놓은 `CommandTree`의 **동기화**가 이루어지지 않은 것인데, `tree`의 명령어들은 `bot.tree.sync()`를 실행해야 디스코드 인터페이스에 동기화가 된다. ```python @bot.event @@ -93,15 +93,15 @@ async def setup_hook(): await bot.tree.sync() ``` -`setup_hook()` 이벤트에 `bot.tree.sync()`가 발동되게 만들어 놓으면 봇 최초 구동 시에 명령어 변경사항이 반영된다. 디스코드 인터페이스를 한 번 새로고침(Ctrl + R)하면 변경된 명령어 목록이 보일 것이다. +`setup_hook()` 이벤트에 `bot.tree.sync()`가 발동되게 만들어 놓으면 봇 **최초 구동 시**에 명령어 변경 사항이 반영된다. 디스코드 인터페이스를 한 번 새로고침(Ctrl + R)하면 변경된 명령어 목록이 보일 것이다. -`setup_hook()`은 `on_ready()`와 비슷하면서도 다르다. `on_ready()`는 봇이 디스코드와 완전히 연결이 된 후 불러오는 이벤트인 반면에, `setup_hook()`은 게이트웨이에는 연결됐지만 웹소켓에는 연결이 이루어지지 않았을 때 발동되기에 다른 이벤트들보다 먼저 발동된다. 또한, `on_ready()`는 연결이 재생성 될 때 다시 발동이 될 수 있지만 `setup_hook()`은 봇 구동 시 1회만 발동된다. +`setup_hook()`는 `on_ready()`와 비슷하면서도 다르다. `on_ready()`는 클라이언트가 디스코드와 **완전한 연결**을 생성했을 때 호출되는 이벤트지만, `setup_hook()`는 클라이언트가 **로그인**을 했을 때 발동되기에 모든 이벤트 중에 가장 먼저 발동된다. 또한, `on_ready()`는 연결이 끊겼다가 재생성 될 때 다시 호출될 수 있지만 `setup_hook()`는 봇 구동 시 **1회만** 호출된다. ![](/assets/img/discord%20bot/4_4.png) -연결이 멈췄다가 재생성 됐을 때 `on_ready()`가 다시 발동된 것을 볼 수 있다. 그러니 최초 1회에 한해 무언가를 하게 시키고 싶다면 `seteup_hook()` 이벤트를 활용하자. +터미널 메시지를 보면 연결이 끊겼다가 재생성 됐을 때 `on_ready()`가 다시 발동하는 것을 확인할 수 있다. 그러니 최초 1회에 한해 무언가를 하게 시키고 싶다면 `on_ready()` 대신에 `seteup_hook()` 이벤트를 활용하자. -자동으로 동기화하게 하고 싶지 않다면 이벤트 대신 명령어로 만들어도 된다. +봇이 자동으로 `tree`를 동기화하지 않고 싶지 않다면 이벤트 대신 명령어로 만들어도 된다. ```python ADMIN = int(os.getenv('ADMIN_ID')) @@ -115,15 +115,15 @@ async def sync(ctx): await ctx.send("권한이 없습니다.") ``` -일반 사용자가 액세스하지 못하도록 명령어를 입력한 사용자가 관리자(나)인지 확인하게 했고, Slash Command에도 일부러 뜨지 않게 했다. 이러면 최초 구동 시 동기화가 이루어지는 것이 아니라 명령어가 입력되었을 때 동기화가 실행된다. +일반 사용자가 액세스하지 못하도록 명령어를 입력한 사용자가 `.env`에서 지정한 **관리자**인지 확인하게 했고, Slash Command가 아니라 **일반 명령어**로 설정해서 일반 사용자들에게 보이지 않게 했다. 이러면 봇 구동 시 자동으로 동기화가 이루어지는 것이 아니라 명령어를 입력하여 **수동**으로 동기화를 진행한다. 4_5 -관리자인 'Experimental' 계정으로 명령어를 입력하니 동기화가 성공적으로 실행되지만 그 외의 사용자는 걸러지는 모습이다. 권한 및 역할별 사용할 수 있는 명령어를 설정하는 것은 추후에 더 자세히 다룰 것이다. +관리자 계정으로 명령어를 입력하니 동기화가 성공적으로 실행되지만, 권한이 없는 사용자는 걸러지는 모습이다. 사용자의 권한 및 길드 내 역할별로 사용할 수 있는 명령어를 설정하는 것은 다음에 더 자세히 다룰 것이다. ### 3. Context Menu 사용하기 -**Context Menu**는 Message Command와 User Command에서 보이는 기능들을 설정하는 데 사용되는 decorator이다. Slash Command는 `@bot.tree`에 `command`를 달아 만들었는데 Context Menu는 `@bot.tree.context_menu`를 사용해 설정한다. 전에 만든 서버 참가일을 보는 명령어를 응용해 먼저 User Command를 만들어 보겠다. +**Context Menu**는 Message Command와 User Command에서 보이는 기능들을 설정하는 데 사용되는 decorator이다. Slash Command는 `@bot.tree.command`를 달아 만들었는데, Context Menu는 `@bot.tree.context_menu`를 사용해 설정한다. 전에 만들었던 서버 참가일을 조회하는 명령어를 응용해 User Command를 먼저 만들어 보겠다. ```python # Slash Command @@ -140,17 +140,17 @@ async def joined_user_menu(interaction: discord.Interaction, member: discord.Mem await interaction.response.send_message(f"{member.display_name}님은 {join_date}에 서버에 참가했습니다.") ``` -Slash Command에서 argument로 조회할 멤버를 알려줘야 했던 것과 달리, User Command는 처음부터 특정 사용자를 선택해 실행하는 거라 별도로 입력해 주어야 할 내용은 없다. +Slash Command에서 argument로 조회할 멤버를 입력해야 했던 것과 달리, User Command는 처음부터 특정 사용자를 선택해 실행하는 거라 별도로 입력해 주어야 할 내용은 없다. ![](/assets/img/discord%20bot/4_6.png) -이제 원하는 멤버의 아바타를 우클릭(모바일에서는 꾹 누르기)하면 안에 방금 추가한 참가일 기능이 있는 것을 볼 수 있다. +이제 원하는 멤버의 아바타를 **우클릭**(모바일에서는 아바타 터치)하면 안에 방금 추가한 참가일 기능이 있는 것을 볼 수 있다. 4_7 -명령어로 하는 것과 똑같이 잘 출력된다. Slash Command랑 User Command에 쓴 함수가 똑같지만 아쉽게도 다수의 Application Command에 하나의 함수를 돌려쓰는 선택지는 없는 듯하다. +명령어로 하던 것과 똑같게 잘 출력된다. 참가일 조회 Slash Command랑 User Command에 쓴 코드가 일치하지만, 아쉽게도 다수의 Application Command에 하나의 함수를 돌려쓰는 선택지는 없는 듯하다. -마지막으로 Message Command를 만들어 보자. 똑같이 Context Menu를 사용하기에 거의 비슷하지만, parameter에서 `discord.Member` 대신 `discord.Message`로 설정해 주어야 한다. +마지막으로 Message Command를 만들어 보자. `@bot.tree.context_menu`를 사용하는 것은 User Command와 동일하지만, parameter에서 `discord.Member` 대신 `discord.Message`로 설정해 주어야 한다. ```python @bot.tree.context_menu(name="글자수") @@ -160,17 +160,17 @@ async def joined(interaction: discord.Interaction, message: discord.Message): await interaction.response.send_message(f"공백 포함 {characters}자, 공백 제외 {characters_no_space}자") ``` -간단하게 글자수를 세주는 기능을 만들었다. `message.content`로 메시지의 내용(string)을 가져와 길이를 확인하고 `len()`으로 글자수를 확인한다. +간단하게 메시지의 글자 수를 세는 기능을 만들었다. `message.content`로 메시지의 내용을 가져와 `len()`으로 글자 수를 확인한다. ![](/assets/img/discord%20bot/4_8.png) -글자수를 확인할 메시지를 우클릭하고 에서 참가일을 선택해 준다. +글자 수를 확인할 메시지를 **우클릭**(모바일에서는 메시지 길게 터치)하고 에서 글자 수를 선택해 준다. 4_9 -*"다람쥐 헌 쳇바퀴에 타고파"* 메시지의 공백 포함 글자수와 공백 제외 글자수가 정상적으로 출력됐다. +선택한 메시지의 공백 포함 글자 수와 공백 제외 글자 수가 정상적으로 출력됐다. -이렇게 Slash Command, User Command, Message Command 세 가지 Application Command 모두 구현하는 데 성공했다. 다음에는 봇의 출력을 더 보기 쉽게, 그리고 예쁘게 만들기 위해 필요한 embed 같은 UI 요소들을 알아볼 것이다. +이렇게 Slash Command와 User Command 및 Message Command 세 가지 Application Command를 모두 구현하는 데 성공했다. 아쉬운 것은 봇이 밋밋하게 텍스트만 출력한다는 점인데, 다음에는 봇의 출력을 더 보기 쉽고 예쁘게 만들기 위해 필요한 UI 요소들을 알아볼 것이다. ## 부록 diff --git a/_posts/2024-06-01-discord-bot-6.md b/_posts/2024-06-01-discord-bot-6.md index 7ef61507ec1..d2ea6404854 100644 --- a/_posts/2024-06-01-discord-bot-6.md +++ b/_posts/2024-06-01-discord-bot-6.md @@ -1,7 +1,7 @@ --- title: 디스코드 봇 DIY - 6. 모듈화와 실시간 기능 추가 date: 2024-06-25 14:42:37 +/-TTTT -last_modified_at: 2024-06-25 16:46:06 +/-TTTT +last_modified_at: 2024-06-27 17:09:56 +/-TTTT categories: [Python, discord.py] tags: [python, discord, bot, modularization] description: Cog로 코드 모듈화와 실시간 수정 두 마리 토끼 잡기 @@ -14,16 +14,16 @@ description: Cog로 코드 모듈화와 실시간 수정 두 마리 토끼 잡 ## Cog와 모듈화 -**Cog**는 `discord.py`에서 **모듈**이나 **익스텐션**을 뜻하는 용어이다. 지금은 `bot.py`에 모든 명령과 기능들이 다 합쳐져 있지만 Cog를 사용하면 카테고리별로 묶어 관리하는 것이 가능해진다. 이러면 자연스럽게 이것저것 다 섞여 있던 `bot.py`처럼 코드가 엉망이 되는 것을 방지할 수 있다. +**Cog**는 `discord.py`에서 **모듈**이나 **익스텐션**을 뜻하는 용어이다. 지금은 `bot.py`에 모든 명령과 기능들이 다 섞여 있지만 Cog를 사용하면 카테고리별로 묶어 관리하는 것이 가능해진다. 이러면 자연스럽게 스파게티 코드가 되는 것도 미연에 방지할 수 있다. > Cog에 대한 예시와 자세한 설명은 [이곳](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html)에서 볼 수 있다. {: .prompt-info} -앞으로 만들 기능들을 생각하면 아직 1%도 안한 느낌이지만, 미리 모듈화하여 더 복잡해지기 전에 관리를 시작하는 편이 좋을 것이다. +앞으로 만들 기능들을 생각하면 아직 1%도 안 한 느낌이지만, 미리 모듈화하여 더 복잡해지기 전에 관리를 시작하는 편이 좋을 것이다. ### 1. Cog 파일 생성하기 -먼저, `bot.py`가 있는 폴더 안에 `cogs/` 폴더를 생성해 준다. 이 폴더 안에 앞으로 만들 Cog 파일들을 전부 보관할 것이다. 멤버가 새로 들어왔을 때 메시지를 출력하는 기능과 환영 Embed를 출력하는 기능은 환영이라는 주제에 들어맞기 때문에 폴더 안에 `welcome.py` 파일을 만들어 Cog를 만들어 보겠다. +먼저, `bot.py`가 있는 폴더 안에 `cogs/` 폴더를 생성해 준다. 이 폴더 안에 앞으로 만들 Cog 파일들을 전부 보관할 것이다. 멤버가 새로 들어왔을 때 메시지를 출력하는 기능과 환영 Embed를 출력하는 기능은 환영이라는 주제를 공유하기 때문에 폴더 안에 `welcome.py` 파일을 만들어 Cog를 만들어 보겠다. ```python # /cogs/welcome.py @@ -71,19 +71,19 @@ async def setup(bot): await bot.add_cog(Welcome(bot)) ``` -Cog는 `discord.ext.commands`에 속해있기 때문에 `commands.Cog`를 상속하는 class를 우선 만들어 준다. `bot.py`에서 이벤트를 확인할 때는 `@bot.event` decorator를 사용했지만, Cog의 경우에는 `@commands.Cog.listener()` decorator를 대신 사용해야 한다. 마찬가지로 Application Command도 `@bot.tree.command` 대신 `@application_commands.command`로 바꿔주면 된다. +Cog도 `discord.ext.commands` 확장 라이브러리에 속해있기 때문에 `commands.Cog`의 subclass를 우선 만들어 준다. 앞서 이벤트를 확인할 때는 `@bot.event` decorator를 사용해 이벤트에 대응하는 함수를 만들었지만, Cog의 경우에는 `@commands.Cog.listener` decorator를 대신 사용해야 한다. 마찬가지로 Slash Command도 `bot.py`에서 사용하던 `@bot.tree.command` 대신 `@application_commands.command`로 바꿔준다. -> 일반적인 `@bot.command()` 명령어는 `@commands.command()`로 바꿔주면 된다. +> `@bot.command` decorator를 사용하는 **일반 명령어**는 `@commands.command`를 대신 사용한다. {: .prompt-info} -> Cog에 추가한 명령어와 실행 모듈에 있는 명령어 **이름이 같으면** 오류가 발생한다. +> Cog에 추가한 명령어와 실행 모듈에 있는 명령어의 **이름이 같으면** 오류가 발생한다. {: .prompt-warning} -Class에 기능들을 넣었다면 마지막에 `setup()`으로 Cog를 Bot에 등록하는 함수를 만들어 주면 끝이다. +Class에 기능들을 넣었다면 마지막에 `setup()`으로 Cog를 Bot에 **등록하는 함수**를 만들어 주면 끝이다. ### 2. 메인 모듈에 Cog 연결하기 -Cog에 기능들을 추가해도 실행하는 건 `welcome.py`이 아니기 때문에 봇에 반영이 되지 않는다. 봇에 추가한 기능들을 반영하려면 실행되는 메인 모듈인 `bot.py`에 Cog를 연결시켜야 한다. +Cog에 기능들을 추가해도 실행하는 건 `welcome.py`이 아니라 `bot.py`이기 때문에 봇에 반영이 되지 않는다. 봇에 추가한 기능들을 반영하려면 실행되는 **메인 모듈**에 Cog를 연결해야 한다. ```python # bot.py @@ -93,7 +93,7 @@ async def load_extensions(): ... ``` -이 함수를 우선 `bot.py`에 추가하자. `load_extension()`은 앞서 `setup()`으로 등록해놓은 Cog를 불러오는 함수로, argument는 Cog 파일 경로에서 확장명을 제외하고 `/`를 `.`으로 대치한 string을 넣어주면 된다. 우리의 경우 경로가 `cogs/welcome.py`이므로 `cogs.welcome`이 된다. +이 함수를 우선 `bot.py`에 추가하자. `load_extension()`은 앞서 Cog 파일에서 `setup()`으로 등록한 Cog를 불러오는 함수로, argument는 Cog 파일 경로에서 **확장명을 제외**하고 `/`을 `.`으로 **대치**한 string을 넣어주면 된다. 우리의 경우 경로가 `cogs/welcome.py`이므로 `cogs.welcome`이 된다. ```python # bot.py @@ -105,11 +105,11 @@ async def setup_hook(): ... ``` -추가한 함수를 `setup_hook` event에 넣어서 봇이 서버에 로그인을 성공했을 때 Cog를 불러올 수 있도록 설정해 주자. `load_extensions()`는 coroutine으로 `async` 함수 안에 추가해야 하는데, `on_ready`은 여러 번 호출될 수 있는 반면에 `setup_hook`은 실행 시 단 한 번만 호출되므로 Cog를 추가하는 목적에 적합하다. (이유는 잘 모르겠지만 `on_ready`에 넣으니 동작을 하지 않는다) +추가한 함수를 `setup_hook()` 이벤트에 넣어서 봇이 서버에 로그인했을 때 Cog를 불러올 수 있도록 설정해 주자. `on_ready()`은 여러 번 호출될 수 있는 반면에 `setup_hook()`은 실행 시 단 한 번만 호출되므로 Cog를 추가하는 목적에 적합하다. ~~이유는 잘 모르겠지만 `on_ready()`에 넣으니 아예 동작을 하지 않는다.~~ ### 3. View 및 인터페이스 Cog로 옮기기 -UI 요소를 추가한 명령어에는 이미 View 때문에 class를 가지고 있다. 이 경우에는 Cog로 어떻게 옮기는지에 대한 방법을 한 번 짚고 넘어가겠다. +UI 요소를 추가한 명령어에는 이미 View의 subclass가 있다. 이 경우에는 Cog로 어떻게 옮기는지에 대한 방법을 한 번 짚고 넘어가겠다. ```python # cogs/interface.py @@ -158,11 +158,11 @@ async def setup(bot): await bot.add_cog(Interface(bot)) ``` -지난번에 인터페이스 테스트 용으로 만들었던 명령어들을 `cogs/` 폴더 안에 `interface.py` 파일을 새로 만들고 옮겨 넣었다. 코드를 전부 Cog class 안에 넣어도 되지만, 위처럼 View class들은 밖으로 빼주고 명령어들만 Cog 안에 넣어도 정상적으로 인터페이스까지 출력된다. 이 점을 활용해 인터페이스는 별도의 파일에 따로 저장하고 필요할 때 모듈을 불러오는 식으로 세분화할 수 있다. +지난번에 인터페이스 테스트용으로 만들었던 명령어들을 `cogs/` 폴더 안에 `interface.py` 파일을 새로 만들고 옮겨 넣었다. 코드를 전부 **Interface Cog** 안에 넣어도 되지만, 위처럼 UI 요소들은 밖으로 빼주고 명령어들만 Cog 안에 넣어도 정상적으로 인터페이스까지 출력된다. 이 점을 활용해 인터페이스는 **별도의 파일**에 따로 저장하고 필요할 때 모듈을 불러오는 식으로 세분화할 수 있다. ### 4. 여러 개의 Cog 한 번에 추가하기 -앞서 `bot.py`에서 `welcome.py` Cog만 불러왔었지만, 기능이 하나 둘 추가될 때마다 Cog의 갯수도 함께 늘어날 것이다. 이에 대비해 `cogs/` 폴더 안의 Cog들을 전부 불러오도록 코드를 고쳐보자. +앞서 `bot.py`에서 `welcome.py` Cog만 불러오도록 했지만, 기능이 하나둘 추가될 때마다 Cog의 개수도 함께 늘어날 것이다. 이에 대비해 `cogs/` 폴더 안의 Cog들을 전부 불러오도록 코드를 고쳐보자. ```python # bot.py @@ -174,13 +174,13 @@ async def load_extensions(): await bot.load_extension(extension) ``` -`cogs/` 폴더 안에 있는 모든 Python 파일을 불러오도록 수정했다. +`cogs/` 폴더 안에 있는 모든 Python 파일을 열어 Cog들을 불러오도록 수정했다. 6_1 `interface.py`를 포함한 모든 Cog가 추가된 것을 터미널에서 확인할 수 있다. -> 폴더 안의 모든 Python 파일에 `setup()` 함수가 없으면 오류가 발생한다. +> `cogs/` 폴더의 **모든 Python 파일**마다 `setup()` 함수가 없으면 오류가 발생한다. {: .prompt-warning} ## 실시간 기능 추가 @@ -189,7 +189,7 @@ Cog의 장점은 모듈화에서 끝나지 않는다. 지금까지 기능을 추 ### 1. Cog Unload 하기 -봇을 구동할 때 불러왔던 Cog들을 unload, 또는 비활성화 시킬 수 있다. `load_extension()` 함수의 반대가 되는 `unload_extension()`을 사용하면 된다. 봇을 멈추지 않고 작업을 수행할 수 있도록 명령어를 추가했다. +봇을 구동할 때 불러왔던 Cog들을 **unload**, 또는 비활성화시킬 수 있다. `load_extension()` 함수의 반대가 되는 `unload_extension()`을 사용하면 된다. 봇을 멈추지 않고 작업을 수행할 수 있도록 명령어를 추가했다. ```python # bot.py @@ -205,9 +205,9 @@ async def unload(ctx, extension: str): ... ``` -일반 사용자들이 접근하지 못하도록 Application Command가 아닌 일반 명령어를 사용했고 `@commands.is_owner()` decorator를 통해 명령어를 입력한 사람이 봇의 소유자인지 확인하는 절차도 추가했다. Parameter로 Cog의 이름을 입력하게 하여 일치하는 Cog를 비활성화하도록 만들었다. +일반 사용자들이 접근하지 못하도록 Slash Command가 아닌 **일반 명령어**를 사용했고 `@commands.is_owner()` decorator를 통해 명령어를 입력한 사람이 봇의 소유자인지 확인하는 절차도 추가했다. Parameter로 Cog의 이름을 입력하게 하여 일치하는 Cog를 비활성화하도록 만들었다. -비활성화를 했으니 다시 활성화할 수 있는 명령어도 추가하자. +비활성화했으니 다시 활성화할 수 있는 명령어도 추가하자. ```python # bot.py @@ -224,7 +224,7 @@ async def unload(ctx, extension: str): ... ``` -Unload할 때와 달리 새로 불러올 때는 tree의 명령어를 수정했을 수 있기 때문에 `bot.tree.sync()`로 동기화를 해주어야 한다. +Unload할 때와 달리 새로 불러올 때는 tree의 명령어를 수정했을 수 있기 때문에 `bot.tree.sync()`로 **동기화**를 해주어야 한다. 6_2 @@ -248,7 +248,7 @@ Unload를 할 때 `teardown()`에 적은 대로 메시지가 출력되는 것을 ### 2. Cog Reload 하기 -`unload_extension()`와 `load_extension()`로 나누어 쓰지 않고 `reload_extension()`으로 한 번에 Cog를 다시 불러올 수 있다. +`unload_extension()`과 `load_extension()`으로 나누어 쓰지 않고 `reload_extension()`으로 한 번에 Cog를 다시 불러올 수 있다. ```python # bot.py @@ -276,19 +276,19 @@ async def reload(ctx, extension: str): ... ``` -봇을 실행하는 도중에 interface에 위처럼 새로운 명령어를 추가한 뒤 reload를 작동시켰다. +봇을 실행하는 도중에 `interface.py` 파일에 위처럼 새로운 명령어를 추가한 뒤 reload를 작동시켰다. 6_4 -성공이다. 봇을 다시 시작하지 않고도 새로 추가한 명령어를 사용할 수 있게 만들었다. +성공이다. 봇을 다시 시작하지 않고도 새로 추가한 명령어를 사용할 수 있게 했다. 6_5 -도중에 오류가 발생하면 간단하게 메시지로 확인할 수도 있다. +도중에 오류가 발생하면 간단하게 메시지로 확인된다. ### 3. 새로운 Cog 추가하기 -이미 존재하는 Cog 파일을 수정하는 것 외에 새로운 파일과 Cog를 생성해 추가하는 것도 가능하다. +이미 존재하는 Cog 파일을 수정하는 것 외에 **새로운 파일과 Cog**를 생성해 추가하는 것도 가능하다. ```python # cogs/new.py @@ -308,13 +308,13 @@ async def setup(bot): await bot.add_cog(New(bot)) ``` -테스트를 위해 봇 동작 중에 `new.py`라는 파일과 **New** Cog를 새로 만들었다. +테스트를 위해 봇 동작 중에 `cogs/` 폴더 안에 `new.py`라는 파일과 **New Cog**를 새로 만들었다. 6_6 -`$load new`를 해주니 문제없이 추가한 명령어를 사용할 수 있다. +`$load new`를 해주니 봇 재시작 없이 추가한 명령어를 사용할 수 있다. -이제 모듈화도 이루었고 끊김 없이 개발을 할 수 있도록 준비를 마쳤으니 본격적으로 각각의 기능들을 추가해 넣을 시간이다. 하지만 아직 준비할 것이 한 가지 남아있는데, 바로 데이터베이스이다. 다음에는 엑셀을 활용해 데이터베이스를 설정하는 방법에 대해 알아보겠다. +이제 모듈화도 이루었고 끊임없이 개발할 수 있도록 준비를 마쳤으니, 본격적으로 각각의 기능들을 추가해 넣을 시간이다. 하지만 아직 준비할 것이 한 가지 남아있는데, 바로 데이터베이스이다. 다음에는 엑셀을 활용해 데이터베이스를 설정하는 방법에 대해 알아보겠다. ## 부록 diff --git a/_posts/2024-06-24-discord-bot-5.md b/_posts/2024-06-24-discord-bot-5.md index eb3f9cccad6..e026c629fcf 100644 --- a/_posts/2024-06-24-discord-bot-5.md +++ b/_posts/2024-06-24-discord-bot-5.md @@ -1,7 +1,7 @@ --- title: 디스코드 봇 DIY - 5. Embed, 버튼, 드롭다운 메뉴 date: 2024-06-24 16:15:09 +/-TTTT -last_modified_at: 2024-06-24 16:15:09 +/-TTTT +last_modified_at: 2024-06-27 16:32:47 +/-TTTT categories: [Python, discord.py] tags: [python, discord, bot, UI] description: UI로 봇의 메시지를 더 예쁘게 만들기 @@ -10,20 +10,20 @@ description: UI로 봇의 메시지를 더 예쁘게 만들기 > 이 글에서 다루는 내용 > - Embed 활용해서 메시지 출력하기 > - 버튼과 드롭다운 메뉴로 입력값 받기 -> - Timeout된 객체 비활성화 시키기 +> - Timeout 된 객체 비활성화시키기 > - Timeout에 면역인 객체 생성하기 ## 봇의 사용성 높이기 -이때까지 만든 기능들은 글로만 메시지를 뱉어내고 글로만 입력을 받는 text-based UI에 불과하다. 물론 디스코드 인터페이스 자체에 여러 종류의 GUI가 있지만, 내 봇과 사용자 간의 입력과 출력을 담당하는 건 밋밋한 텍스트 뿐이다. 이 글에서는 봇의 사용성을 높이기 위해 메시지를 꾸미고, 타이핑 없이 봇과 상호작용할 수 있는 방법들에 대해 다룰 것이다. +지금까지 만든 기능들은 글로만 메시지를 뱉어내고 글로만 입력을 받는 **Text-based UI**에 불과하다. 물론 디스코드 인터페이스 자체에 여러 종류의 GUI가 있지만, 내 봇과 사용자 간의 입력과 출력을 담당하는 건 밋밋한 텍스트뿐이다. 이 글에서는 봇의 사용성을 높이기 위해 메시지를 꾸미고, 타이핑 없이 봇과 상호 작용을 할 수 있는 방법들에 대해 다룰 것이다. ### 1. Embed로 메시지 보내기 -디스코드를 좀 써봤다면 링크를 보내거나 봇과 상호작용할 때 창을 띄운 것처럼 상자 안에 글과 사진 등이 들어가 있는 메시지를 본 적이 있을 것이다. 그런 형태의 메시지가 바로 **Embed**이다. +디스코드를 좀 써봤다면 링크를 보내거나 봇과 상호 작용을 할 때 창을 띄운 것처럼 상자 안에 글과 사진 등이 들어가 있는 메시지를 본 적이 있을 것이다. 그런 형태의 메시지가 바로 **Embed**이다. ![](/assets/img/discord%20bot/5_1.png) -위의 이미지처럼 Embed는 박스와 색깔 테두리가 존재해서 일반 메시지보다 시인성이 좋다. 내용에는 텍스트는 물론, **이미지와 동영상** 등도 첨부할 수 있다. 일반 사용자들의 채팅은 단순한 글이기 때문에 Embed를 활용하면 채팅과 봇의 출력 메시지를 구분 지어 보기 편하다. Embed에 들어갈 수 있는 요소들을 아래 표에 정리해 보았다. +위의 이미지처럼 Embed는 박스와 색깔 테두리가 존재해서 일반 메시지보다 시인성이 좋다. 내용에는 텍스트는 물론, **이미지와 동영상** 등도 첨부할 수 있다. 일반 사용자들의 채팅은 주로 단순한 글이기 때문에 Embed를 활용하면 채팅과 봇의 출력 메시지를 구분 지어 보기 편하다. Embed에 들어갈 수 있는 요소들을 아래 표에 정리해 보았다. | 요소 | 설명 | | :------------ | :---------------------------------- | @@ -41,20 +41,20 @@ description: UI로 봇의 메시지를 더 예쁘게 만들기 | `url` | 첨부할 링크 | | `video` | 첨부할 영상 | -> Embed의 텍스트 요소들에는 **글자수 제한**이 있다. 자세한 내용은 [여기](https://discord.com/developers/docs/resources/channel#embed-object-embed-limits)를 참고. +> Embed의 텍스트 요소들에는 **글자 수 제한**이 있다. 자세한 내용은 [여기](https://discord.com/developers/docs/resources/channel#embed-object-embed-limits)를 참고. {: .prompt-warning} -이 구성 요소들 중에 꼭 있어야 하는 건 `description` 하나뿐이고 나머지는 상황에 맞게 사용하면 된다. Embed에 요소들을 추가하는 게 꽤 간편하기 때문에 손쉽게 메시지를 깔쌈하게 만들어 보낼 수 있다. 글의 줄바꿈 여부, 사진의 크기 같은 것들도 설정할 수 있으므로 여러모로 활용하기 좋은 물건이다. +이 구성 요소 중에 필수로 있어야 하는 건 `description` 하나뿐이고 나머지는 상황에 맞게 사용하면 된다. Embed에 요소들을 추가하는 게 꽤 간편하기 때문에 손쉽게 메시지를 깔쌈하게 만들어 보낼 수 있다. 글의 줄 바꿈 여부, 사진의 크기 같은 것들도 설정할 수 있으므로 여러모로 활용하기 좋은 물건이다. > discord.py에서의 Embed 사용 관련 내용은 [이곳](https://discordpy.readthedocs.io/en/latest/api.html?#embed)을 참고하면 된다. {: .prompt-info} 5_2 -*출처: [b1naryth1ef.github.io](https://b1naryth1ef.github.io/disco/bot_tutorial/message_embeds.html)* +*출처: [b1naryth1ef](https://b1naryth1ef.github.io/disco/bot_tutorial/message_embeds.html)* -구성 요소들이 Embed에서 어디에 위치하는지는 위의 사진에서 확인할 수 있다. 요소들의 위치는 각자 자리에 **고정**되어 있으며 **변경이 불가능**하다. 예를 들어 썸네일은 항상 우측 상단에 위치하고 첨부된 이미지는 항상 하단에 나온다. 커스터마이징 측면에서는 조금 아쉽지만 덕분에 모든 Embed들은 통일성 있는 모습을 보인다. +구성 요소들이 Embed에서 어디에 위치하는지는 위의 사진에서 확인할 수 있다. 요소들의 위치는 각자의 자리에 **고정**되어 있으며 변경이 불가능하다. 예를 들어 썸네일은 항상 우측 상단에 위치하고 첨부된 이미지는 항상 하단에 나온다. 커스터마이징 측면에서는 조금 아쉽지만, 이 특징 덕분에 모든 Embed는 통일된 모습을 보인다. -일전에 만든 `hello` Slash Command가 Embed를 출력하도록 코드를 고쳐보자. +일전에 만든 **/hello** Slash Command가 Embed를 출력하도록 코드를 고쳐보자. ```python from datetime import datetime # timestamp 시간 확인용 @@ -86,22 +86,22 @@ async def hello(interaction: discord.Interaction): ![](/assets/img/discord%20bot/5_3.png) -거의 모든 요소들을 추가해서 Embed를 만들어 보았다. 가장 먼저 `embed = discord.Embed()`로 Embed를 생성하면서 제목과 설명, 색깔, 시간과 링크를 설정해 주었는데, 보편적으로는 `title`, `description`, `color` 세 가지를 활용한다. 색깔은 **헥스 코드**를 사용하거나 위처럼 [`discord.Color`](https://discordpy.readthedocs.io/en/latest/api.html?highlight=colour#discord.Colour)를 사용하면 된다. +추가 가능한 거의 모든 요소를 추가해서 Embed를 만들어 보았다. 가장 먼저 `embed = discord.Embed()`로 Embed를 생성하면서 제목과 설명, 색깔, 시간과 링크를 설정해 주었는데, 보편적으로는 `title`, `description`, `color` 세 가지를 활용한다. 색깔은 **헥스 코드**를 사용하거나 위처럼 [`discord.Color`](https://discordpy.readthedocs.io/en/latest/api.html?highlight=colour#discord.Colour)를 사용하면 된다. -**Field**는 Embed에서 사용하는 **텍스트 상자**라고 생각하면 되는데, Embed를 생성할 때의 `title`과 `description`에 맞먹는 `name`과 `value`를 설정해야 한다. `inline`은 줄바꿈 설정으로, `False`면 줄바꿈을 하고 `True`면 줄을 바꾸지 않고 다음 field를 추가한다. 때문에 `inline=False`인 첫 field는 줄 하나에 홀로 들어가 있고 `inline=True`인 다음 세 개의 field는 줄 하나를 나눠 쓴다. +**Field**는 Embed에서 사용하는 **텍스트 상자**라고 생각하면 되는데, Embed의 `title`과 `description`에 맞먹는 `name`과 `value`를 설정해야 한다. `inline`은 **줄 바꿈 설정**으로, `False`면 줄을 바꾸고 `True`면 줄을 바꾸지 않은 채로 다음 field를 추가한다. 그 때문에 `inline=False`인 첫 field는 줄 하나에 홀로 들어가 있고 `inline=True`인 다음 세 개의 field는 줄 하나를 나눠 쓴다. 단, 설정과 관계없이 Embed의 최대 너비를 벗어나면 자동으로 줄이 바뀐다. > 테스트에 사용할 이미지가 없거나 원하는 규격의 이미지를 쓰고 싶으면 [이 사이트](https://picsum.photos/)를 활용해도 좋다. {: .prompt-tip} -그 외의 요소들은 `set_author()`처럼 따로 만들어 주면 된다. 이런 텍스트 요소에도 `url` argument를 써 링크를 추가할 수도 있다. 이미지는 로컬에 저장된 이미지를 사용하거나 URL을 활용할 수도 있다. 로컬에서 불러오는 경우 예시처럼 `send_message()`에서 `file`을 지정해야 한다. +그 외의 요소들은 `embed.set_author()`처럼 따로 만들어 주면 된다. 이런 텍스트 요소에도 `url` argument를 추가해 링크를 넣을 수 있다. 이미지는 **로컬 폴더**에 저장된 이미지를 사용하거나 **이미지 URL**을 활용할 수 있다. 로컬에서 불러오는 경우 예시처럼 `send_message()`에서 `file`을 지정해야 한다. -이렇게 설정을 끝마쳤으면 원래 메시지를 보낼 때 쓰던 `send_message()`에 `embed` argument를 지정해 주면 된다. 예시에서는 원래 메시지인 `"안녕하세요"`를 같이 넣었지만 메시지와 Embed를 같이 쓸 수 있는 것을 보여주려 넣은 거라 지우고 embed만 사용하면 된다. +이렇게 설정을 끝마쳤으면 텍스트를 보낼 때 쓰던 `send_message()`에 제작한 `embed`를 argument로 지정해 주면 된다. 예시에서는 원래 메시지인 "안녕하세요"를 함께 썼지만, 텍스트 메시지 없이 Embed만 보낼 수 있다. ### 2. 버튼 추가하기 -Embed는 디스플레이 용도로 사용되고 이것으로 봇에게 **입력값을 주는 상호작용**은 불가능하다. 그래서 입력이 필요할 때는 메시지에 **Message Component**라는 UI를 추가로 설정한다. 이 부분은 라이브러리 중 `discord.ui`가 담당하고 있는데, Message Component에 해당하는 버튼과 드롭다운 메뉴 같은 UI 요소를 첨부하려면 메시지에 달린 **View**라는 기능을 활용해야 한다. +Embed는 봇의 **출력을 디스플레이**하는 용도로 사용하는 데 특화되어 있고 봇에게 **입력값**을 주는 상호 작용은 불가능하다. 그래서 입력이 필요할 때는 메시지에 **Message Component**라는 UI 요소를 추가로 설정한다. 이 부분은 `discord.py` 라이브러리 중 `discord.ui`가 담당하고 있는데, 버튼과 드롭다운 메뉴 같은 Message Component를 메시지에 첨부하려면 `discord.ui`의 **View**라는 요소를 활용해야 한다. -View를 표시하려면 Embed처럼 `send_message()`에서 값을 설정해야 한다. View마다 최대 다섯 줄을 넣을 수 있는데, 줄마다 드롭다운 메뉴 1개 혹은 버튼 최대 5개를 넣을 수 있다. 즉, 한 View에 넣을 수 있는 최대 버튼 갯수는 25개이다. +View를 표시하려면 Embed처럼 `send_message()`에서 argument로 지정해야 한다. View는 Message Component를 배치할 수 있는 줄을 **다섯 줄**씩 보유하고 있는데, 줄마다 드롭다운 메뉴 한 개 혹은 버튼 최대 다섯 개를 배치할 수 있다. 즉, 하나의 View에 담을 수 있는 최대 버튼 개수는 스물다섯 개이다. ```python class ButtonView(discord.ui.View): @@ -117,11 +117,11 @@ async def button(interaction: discord.Interaction): > 만약 `ModuleNotFoundError`가 뜬다면 `pip install discord-ui`로 라이브러리를 설치해야 한다. {: .prompt-warning} -누르면 메시지가 나오는 버튼을 만들고 해당 버튼을 표시하는 명령어를 만들었다. `ButtonView`이라는 `discord.ui.View`의 subclass를 생성하고 그 안에 버튼을 추가한 뒤 `send_message()`의 `view` argument에 `ButtonView()`을 지정했다. +누르면 메시지가 나오는 버튼을 만들고 해당 버튼을 채팅창에 첨부하는 명령어를 만들었다. 버튼을 배치할 수 있도록 `discord.ui.View`의 subclass를 생성하고 그 안에서 버튼 객체를 추가한 뒤 `send_message()`의 `view` argument에 생성한 View 객체를 지정했다. 5_4 -이렇게 버튼을 누르면 `@discord.ui.button()` decorator가 붙은 `button_response()` 함수가 호출되며 메시지가 전송된다. +이렇게 버튼을 누르면 `@discord.ui.button` decorator가 붙은 `button_response()` 함수가 호출되며 정해진 메시지가 전송된다. ![](/assets/img/discord%20bot/5_5.png) @@ -129,7 +129,7 @@ Decorator에서 버튼의 색상도 `style`로 정해줄 수 있다. `discord.py 5_6 -버튼에는 기본적으로 180초의 `timeout`이 적용된다. 이 이후에는 버튼을 눌러도 **상호작용이 실패**했다는 메시지만 뜨고 정상적으로 실행이 되지 않는다. `timeout`이 되더라도 생긴 것은 똑같기 때문에 어떤 버튼이 아직 사용 가능한지 가끔 헷갈리는데, 그걸 방지하기 위해 사용이 끝나거나 `timeout`된 버튼은 비활성화 시킨다. +버튼에는 기본적으로 180초 이후에 **Timeout**이 적용된다. Timeout 상태에서는 버튼을 눌러도 **상호 작용에 실패**했다는 경고만 전송되고 함수가 실행되지 않는데, **외형**은 바뀌지 않기 때문에 어떤 버튼이 아직 사용 가능한지 헷갈릴 수 있다. 이것을 방지하기 위해 사용이 끝나거나 Timeout 상태로 접어든 버튼은 비활성화해 보겠다. ```python class ButtonView(discord.ui.View): @@ -164,11 +164,11 @@ async def button(interaction: discord.Interaction): 5_7 -먼저 `ButtonView`에 `timeout` 값을 설정하도록 했고, `timeout` 전에 버튼을 눌렀으면 `"성공!"` 텍스트를 띄우면서 `button.disabled = True`로 버튼을 비활성화한다. 버튼 클릭 없이 10초가 지나 `on_timeout()`이 호출될 때에도 상응하는 텍스트와 함께 버튼이 비활성화 된다. 비활성화 된 버튼은 색상이 흐려지며 누르는 게 불가능해진다. +먼저, `ButtonView` 객체를 생성할 때 몇 초 후에 Timeout을 할지 `timeout` 값을 정해 설정하도록 했고, Timeout 전에 버튼을 눌렀으면 "성공!" 텍스트를 띄우면서 `button.disabled = True`로 버튼을 비활성화한다. 버튼 클릭 없이 10초가 지나 `on_timeout()` 함수가 호출될 때도 상응하는 텍스트와 함께 버튼이 비활성화된다. 비활성화된 버튼은 색상이 흐려지며 누르는 게 불가능하다. ### 3. 드롭다운 메뉴 만들기 -디스코드의 **Select Menu**는 우리가 흔히 드롭다운 메뉴라고 하는 사용자에게 선택지를 주는 방법 중 하나다. 버튼과 마찬가지로 View에 추가해서 첨부하는 방식이다. +디스코드의 **Select Menu**는 우리가 흔히 드롭다운 메뉴라고 부르는 사용자에게 선택지를 주는 방법의 하나다. 버튼과 마찬가지로 View에 추가해서 첨부하는 방식이다. ```python class SelectView(discord.ui.View): @@ -206,15 +206,15 @@ async def country(interaction: discord.Interaction): 5_9 -아직 선택을 하지 않았을 때는 `placeholder`에 넣은 메시지가 표시된다. 여기는 유저가 Select Menu에서 어떤 항목을 골라야 하는지에 대한 정보를 직관적으로 보여주는 것이 좋다. 고를 수 있는 항목은 `discord.SelectOption` 객체 리스트를 만들어 `options` parameter에 추가하면 된다. +아직 선택을 하지 않았을 때는 `placeholder`에 넣은 메시지가 표시된다. 여기서는 사용자가 Select Menu에서 어떤 항목을 골라야 하는지에 대한 정보를 직관적으로 보여주는 것이 좋다. 고를 수 있는 항목은 `discord.SelectOption` 객체 리스트를 만들어 `options` parameter에 추가하면 된다. `SelectOption` attribute 중 `label`은 제목, `description`은 설명이고, 위에서 국기를 추가한 것처럼 `emoji`로 이모지를 붙일 수도 있다. 이외에도 `value`로 선택 시 전달할 값을 지정할 수 있으며, `default=True`로 설정하면 `placeholder` 대신에 해당 `SelectOption` 항목이 기본으로 보이게 된다. ### 4. Action Row 지정하기 -앞서 View마다 다섯 줄을 넣을 수 있다고 했었는데, 이 줄을 **Action Row**라고 한다. 기본적으로 버튼과 드롭다운 메뉴 같은 요소들이 자동으로 배열이 되는데 `row` parameter를 사용해 0과 4 사이의 값으로 상대적인 줄을 설정할 수 있다. +앞서 View마다 Message Component를 담을 수 있는 줄이 다섯 줄 있다고 했는데, 이 줄을 **Action Row**라고 한다. 기본적으로 버튼과 드롭다운 메뉴 같은 요소들이 자동으로 배열이 되는데 `row` parameter를 사용해 0과 4 사이의 값으로 상대적인 줄을 설정할 수 있다. -```Python +```python class ActionRowView(discord.ui.View): @discord.ui.button(label="버튼 1", row=0, style=discord.ButtonStyle.primary) async def first_button_callback(self, interaction, button): @@ -248,20 +248,20 @@ async def country(interaction: discord.Interaction): 5_10 -`row=0`으로 설정된 버튼 두 개는 같은 줄에 배치됐고 `row=2`로 설정된 버튼 하나는 다른 줄에 배치됐다. Select Menu는 `row=1`으로 설정되었기 때문에 `row=0`과 `row=2` 사이에 배치됐다. 절대적이 아니라 **상대적**이기 때문에 `row=1`이라고 두 번째 줄에 나타나는 것은 아니다. 단지 값이 더 크면 더 아래에 배치되는 것일 뿐이다. +`row=0`으로 설정된 버튼 두 개는 같은 줄에 배치됐고 `row=2`로 설정된 버튼 하나는 다른 줄에 배치됐다. Select Menu는 `row=1`로 설정되었기 때문에 `row=0`과 `row=2` 사이에 배치됐다. 줄 순서는 절대적이 아니라 **상대적**이기 때문에 `row=1`이라고 두 번째 줄에 나타나는 것은 아니다. 단순히 값이 더 크면 더 아래에 배치되는 것일 뿐이다. -같은 줄의 버튼과 버튼 사이를 띄우거나 줄과 줄 사이에 공간을 두는 것은 불가능하다. 위에 만든 `버튼3`의 `row` 값을 3이나 4로 바꿔도 사이에 다른 객체가 없는 한 배치는 변동이 없다. 더불어 한 줄에 다섯 칸이 넘어가게 객체를 할당하면 오류가 발생한다. +같은 줄의 버튼과 버튼 사이를 띄우거나 줄과 줄 사이에 공간을 두는 것은 불가능하다. 위에 만든 `버튼3`의 `row` 값을 3이나 4로 바꿔도 사이에 다른 객체가 없는 한 배치는 **변동이 없다**. 더불어 한 줄에 다섯 칸이 넘어가게 객체를 할당하면 오류가 발생한다. ### 5. 영속적인 View 생성하기 -View는 timeout 값을 `None`으로 설정하더라도 15분 정도 유효하고 봇이 오프라인이 되면 작동을 멈춘다. 메시지로 보내진 View는 채널에 그대로 남아있는데 봇이 다시 온라인이 되더라도 상호작용을 할 수는 없다. 자동 역할 지정 기능처럼 항상 작동이 필요한 View가 있다면 이 점은 굉장히 치명적이다. 하지만 다행히도 우리에게는 해결법이 있다. View에 **영속성**(persistence)을 부여하면 된다. +View는 `timeout` 값을 `None`으로 설정하더라도 약 15분 정도 유효하고 봇이 오프라인이 되면 작동을 멈춘다. 메시지로 보내진 View는 채널에 그대로 남아있지만, 봇의 연결이 끊겼다 돌아오더라도 상호 작용을 할 수는 없다. 자동 역할 지정 기능처럼 **항시 작동**이 필요한 기능에서 View를 사용한다면 이 점은 굉장히 치명적이다. 하지만 다행히도 우리에게는 해결법이 있다. View에 **영속성(Persistency)**을 부여하면 된다. -위에서 만들었던 `SelectView()`를 영속적이게 만들어보자. +위에서 만들었던 `SelectView`를 영속적으로 만들어보자. -```Python +```python class SelectView(discord.ui.View): def __init__(self): - super().__init__(timeout=None) # timeout 없애기 + super().__init__(timeout=None) # Timeout 없애기 self.message = None @discord.ui.select( @@ -275,9 +275,9 @@ async def country(interaction: discord.Interaction): await interaction.response.send_message(view=view) ``` -먼저, `timeout=None`으로 timeout 상태가 되지 않도록 만들어 주고 객체에 `custom_id`를 지정해야 한다. +먼저, `timeout=None`으로 Timeout 상태가 되지 않도록 만들어 주고 객체에 `custom_id`를 지정해야 한다. -```Python +```python @bot.event async def on_ready(): bot.add_view(SelectView()) # 해당 View를 영속적인 객체로 지정한다 @@ -292,15 +292,16 @@ async def on_ready(): 5_11 -봇을 멈췄다가 다시 실행한 뒤 Select Menu를 사용해 보았다. 영속성이 적용되지 않은 View는 위처럼 상호작용이 실패하지만, 아래의 영속적인 View는 정상적으로 작동한다. +봇을 멈췄다가 다시 실행한 뒤 Select Menu를 사용해 보았다. 영속성이 적용되지 않은 View는 위처럼 callback에 실패하지만, 아래의 영속적인 View는 정상적으로 작동한다. -UI를 추가하니 봇이 더욱 풍부해지는 느낌이지만 이와 함께 우리의 코드도 지저분해지고 있다. 이 문제를 해결하기 위해 다음은 기능을 분리해 관리할 수 있는 Cog에 대해 알아볼 것이다. +UI를 추가하니 봇이 더욱 풍부해지는 느낌이지만 요소가 추가되어 갈수록 우리의 코드가 지저분해지고 있다. 이 문제를 해결하기 위해 다음은 기능을 분리해 관리할 수 있는 Cog에 대해 알아볼 것이다. ## 부록 ### i. 전체 코드 -``` Python +``` python +# bot.py import os, discord from discord import app_commands from discord.ext import commands