Discord.py Components V2 사용 가이드 (신형 임베드, 줄 나누기, 임베드 내에 버튼 등)

2025. 12. 6. 16:57·봇 개발 팁/Discord.py

1. Components V2 아키텍처 및 LayoutView 시스템

Discord API의 Components V2는 기존의 자동 레이아웃 방식에서 개발자가 직접 UI를 배치하는 수동 레이아웃 방식으로 바뀌었습니다.
discord.py 2.6부터 추가된 discord.ui.LayoutView가 핵심이며, 기존 discord.ui.View의 자동 행 관리 방식을 완전히 바꿉니다.

1.1 LayoutView 기본 구조

LayoutView는 Components V2에서 UI를 만들 때 사용하는 기본 컨테이너입니다. 기존 View는 컴포넌트를 추가하면 자동으로 5개의 Action Row에 배치했지만, LayoutView에서는 개발자가 직접 계층 구조와 배치를 정해야 합니다.

 

즉, UI 구성의 제어권이 라이브러리에서 개발자에게 넘어온 것입니다. 가장 중요한 제약은 컴포넌트 개수 제한인데, LayoutView 하나당 최대 40개까지 배치할 수 있습니다(중첩 포함). 기존 25개(5×5)보다 늘어났지만, Container나 Section 같은 구조 컴포넌트도 카운트되므로 실제 사용 가능한 슬롯은 생각보다 적을 수 있습니다.

 

특성 discord.ui.View (V1) discord.ui.LayoutView (V2)
기본 레이아웃 자동 수동
행(Row) 관리 자동 처리 ActionRow 명시 필수
최대 컴포넌트 25개 (5x5) 40개 (중첩 포함)
계층 구조 평면적 중첩 가능
Top-Level 요소 Button, Select TextDisplay, Container, Section, ActionRow 등

1.2 클래스 기반 UI 정의

LayoutView는 클래스 속성으로 UI를 정의하는 방식을 사용합니다. 코드 가독성이 좋고 UI 구조를 한눈에 파악하기 쉽습니다.

import discord
from discord import ui

class TechnicalSpecLayout(ui.LayoutView):
    # Top-Level Component 1: TextDisplay
    header = ui.TextDisplay(
        label="시스템 상태 모니터링",
        style=discord.TextStyle.heading
    )

    # Top-Level Component 2: Separator
    separator = ui.Separator(spacing=discord.SeparatorSpacing.small)

    # Top-Level Component 3: Container (Nested)
    status_box = ui.Container(
        ui.TextDisplay("정상 작동 중: 99.9% Uptime")
    )

 

TextDisplay, Separator, Container 같은 V2 컴포넌트는 LayoutView의 최상위에 직접 배치할 수 있습니다. 하지만 Button이나 Select Menu는 최상위에 둘 수 없고, 반드시 ActionRow 안에 넣어야 합니다.

1.3 동적 아이템 관리와 ID 시스템

Components V2의 모든 컴포넌트는 고유한 숫자형 id를 가집니다(custom_id와는 별개). 중첩된 구조에서 특정 컴포넌트를 찾으려면 find_item(id) 메서드를 사용하면 됩니다.

# ID 기반 컴포넌트 탐색 예시
TRACKER_ID = 10101

class DynamicCounter(ui.LayoutView):
    def __init__(self):
        super().__init__()
        self.count = 0

    # ID를 명시적으로 할당하여 추후 검색 가능하게 설정
    display = ui.TextDisplay("Count: 0", id=TRACKER_ID)

    actions = ui.ActionRow()

    @actions.button(label="증가")
    async def increment(self, interaction: discord.Interaction, button: ui.Button):
        self.count += 1
        # ID를 사용하여 깊은 계층의 컴포넌트 탐색
        display_item = self.view.find_item(TRACKER_ID)
        if isinstance(display_item, ui.TextDisplay):
            display_item.label = f"Count: {self.count}"
        await interaction.response.edit_message(view=self)

 

이 ID 시스템 덕분에 UI 상태가 바뀔 때 전체를 다시 만들지 않고 특정 컴포넌트만 수정할 수 있습니다. walk_children()로 모든 자식 컴포넌트를 순회할 수도 있어서 일괄 변경이나 검증 로직 구현에 유용합니다.

2. 텍스트 표시 컴포넌트

기존에는 Embed로 텍스트를 표시했지만, Components V2에서는 TextDisplay라는 전용 컴포넌트가 생겼습니다. 메시지 본문과 별개로 UI 레이아웃 안에 텍스트를 넣을 수 있어서 더 유연합니다.

2.1 TextDisplay 사용법

ui.TextDisplay는 Discord 마크다운을 지원하며, UI 안에서 라벨, 설명, 경고 문구 등을 표시할 수 있습니다.

  • Markdown 지원: Bold(**), Italic(*), Underline(__), Codeblock(```) 등을 포함한 표준 Discord Markdown을 렌더링합니다.
  • 배치 제약: 최상위 레벨에 위치하거나 Container, Section 내부에 중첩될 수 있습니다. ActionRow 내부에는 배치할 수 없습니다.
# TextDisplay 활용 패턴
class InfoLayout(ui.LayoutView):
    # 일반 텍스트
    simple_text = ui.TextDisplay("기본 안내 메시지입니다.")

    # 마크다운 활용 강조
    alert_text = ui.TextDisplay(
        "**경고:** 이 작업은 되돌릴 수 없습니다.\n`System.reset()` 함수가 호출됩니다."
    )

 

TextDisplay는 클릭 같은 상호작용이 안 되는 정적 컴포넌트입니다. custom_id로 이벤트 콜백을 등록할 수 없고, 정보 표시용으로만 씁니다.

2.2 Separator로 간격 조정하기

ui.Separator는 컴포넌트 사이의 간격을 조정하는 요소입니다. UI에서 여백은 가독성에 중요한데, V2에서는 이걸 명시적인 컴포넌트로 제공합니다.

Separator는 spacing과 visible 두 가지 핵심 속성을 통해 제어됩니다.

속성 (Attribute) 타입 (Type) 설명 (Description)
spacing SeparatorSpacing 여백의 크기. small 또는 large 값을 가집니다.
visible bool True일 경우 가로 구분선(Divider) 렌더링. False일 경우 투명 여백만 적용됩니다.
class FormattingLayout(ui.LayoutView):
    title = ui.TextDisplay("설정 메뉴")

    # 가시적인 구분선 (Divider)
    line = ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small)

    option_1 = ui.TextDisplay("옵션 A")

    # 투명한 큰 여백 (Spacer 역할)
    spacer = ui.Separator(visible=False, spacing=discord.SeparatorSpacing.large)

    footer = ui.TextDisplay("Copyright 2025")

 

discord.SeparatorSpacing은 small과 large 두 가지가 있습니다. small은 항목 간 작은 간격에, large는 큰 구분에 씁니다. visible=True와 spacing=large를 같이 쓰면 섹션을 명확하게 나눌 수 있습니다.

3. 컨테이너와 그룹화

Components V2에서는 Section과 Container로 컴포넌트를 그룹화할 수 있습니다.

3.1 Section: 텍스트와 액세서리 조합

ui.Section은 텍스트와 액세서리 컴포넌트 하나를 가로로 배치하는 레이아웃입니다. 설정 화면의 "라벨 - 버튼" 패턴을 만들 때 유용합니다.

  • Content: 섹션 좌측에 표시될 텍스트입니다. 문자열을 전달하면 내부적으로 TextDisplay로 변환되고, TextDisplay 객체를 직접 전달할 수도 있습니다.
  • Accessory: 섹션 우측에 표시될 상호작용 요소입니다. 주로 Button이나 Thumbnail을 씁니다. Select Menu는 액세서리로 쓸 수 없습니다.
class SettingsLayout(ui.LayoutView):
    # 문자열 직접 전달 및 버튼 액세서리
    toggle_section = ui.Section(
        "자동 응답 모드 활성화",
        accessory=ui.Button(label="ON", style=discord.ButtonStyle.success)
    )

    # TextDisplay 객체 전달 및 썸네일 액세서리
    profile_section = ui.Section(
        ui.TextDisplay("**User:** AbstractUmbra"),
        accessory=ui.Thumbnail(url="[https://example.com/avatar.png](https://example.com/avatar.png)") # 가상의 Thumbnail 컴포넌트
    )

 

Section을 쓰면 ActionRow와 TextDisplay를 따로 배치하지 않아도 리스트 형태의 UI를 쉽게 만들 수 있습니다.

3.2 Container: 테두리가 있는 박스

ui.Container는 다른 컴포넌트들을 담을 수 있는 박스입니다. Embed처럼 테두리가 있고, accent_color로 테두리 색상을 바꿀 수 있습니다.

Container는 중첩해서 복잡한 레이아웃을 만들 수 있습니다.

# Container 서브클래싱 패턴
class UserCard(ui.Container):
    def __init__(self, username: str, role_color: int):
        super().__init__(accent_color=role_color)

        # 컨테이너 내부 컴포넌트 정의
        self.name = ui.TextDisplay(f"Name: {username}")
        self.divider = ui.Separator(visible=True)
        self.status = ui.TextDisplay("Status: Online")

class Dashboard(ui.LayoutView):
    # 인스턴스화하여 LayoutView에 배치
    card_1 = UserCard("Admin", 0xFF0000)

    # 빈 공간
    space = ui.Separator(spacing=discord.SeparatorSpacing.large)

    card_2 = UserCard("Moderator", 0x00FF00)

 

Container 안에 Container를 또 넣을 수도 있지만, 컴포넌트 제한(40개)과 모바일 가독성을 생각하면 중첩은 최소화하는 게 좋습니다.

4. 상호작용 컴포넌트와 ActionRow

Components V2에서 Button과 Select Menu의 동작 방식은 V1과 같지만, 배치 방식에는 제약이 있습니다.

4.1 ActionRow 필수 사용

V1에서는 View에 버튼을 추가하면 자동으로 행이 생성됐지만, V2에서는 Button과 Select Menu를 최상위에 둘 수 없습니다. 반드시 ui.ActionRow 안에 넣어야 합니다. ActionRow 하나당 최대 5개 슬롯을 가집니다.

  • Button: 1개 슬롯 차지 (최대 5개/Row)
  • Select Menu: 5개 슬롯 전체 차지 (일반적). 따라서 하나의 ActionRow에는 하나의 Select Menu만 배치 가능합니다.

4.2 데코레이터로 ActionRow 구성하기

ActionRow 인스턴스를 만들고, 메서드 데코레이터로 버튼과 셀렉트 메뉴를 추가하는 게 기본 패턴입니다.

4.2.1 Button 구현

class InteractionLayout(ui.LayoutView):
    # ActionRow 선언
    actions = ui.ActionRow()

    # 데코레이터로 버튼 부착
    @actions.button(label="승인", style=discord.ButtonStyle.primary)
    async def approve(self, interaction: discord.Interaction, button: ui.Button):
        await interaction.response.send_message("승인되었습니다.", ephemeral=True)

    @actions.button(label="거절", style=discord.ButtonStyle.danger)
    async def deny(self, interaction: discord.Interaction, button: ui.Button):
        await interaction.response.send_message("거절되었습니다.", ephemeral=True)

4.2.2 Select Menu 구현

select 데코레이터를 사용하여 String Select, User Select, Role Select 등 다양한 드롭다운 메뉴를 구현합니다.

class SelectionLayout(ui.LayoutView):
    # Select 전용 Row
    select_row = ui.ActionRow()

    @select_row.select(
        placeholder="카테고리 선택",
        min_values=1,
        max_values=1,
        options=
    )
    async def category_select(self, interaction: discord.Interaction, select: ui.Select):
        chosen = select.values
        await interaction.response.send_message(f"선택: {chosen}")

 

채널이나 사용자를 선택하는 특수 셀렉트 메뉴는 cls 파라미터를 통해 타입을 지정합니다.

    @select_row.select(cls=ui.ChannelSelect, channel_types=)
    async def channel_pick(self, interaction: discord.Interaction, select: ui.ChannelSelect):
        #... 구현...

4.3 동적으로 ActionRow 만들기

데코레이터 대신 add_item 메서드로 런타임에 동적으로 ActionRow를 구성할 수도 있습니다.

def create_dynamic_row(labels: list[str]) -> ui.ActionRow:
    row = ui.ActionRow()
    for label in labels:
        btn = ui.Button(label=label)
        # 콜백 별도 지정 필요
        row.add_item(btn)
    return row

class DynamicLayout(ui.LayoutView):
    def __init__(self, options):
        super().__init__()
        # 초기화 시점에 동적 Row 추가
        self.add_item(create_dynamic_row(options))

5. 미디어 갤러리 (MediaGallery)

Components V2에서는 ui.MediaGallery로 첨부 파일을 갤러리 형태로 보여줄 수 있습니다.

5.1 MediaGallery 구성

MediaGallery는 MediaGalleryItem 객체들의 집합으로 구성됩니다. 각 아이템은 URL을 통해 외부 이미지를 참조하거나, attachment:// 스키마를 통해 업로드된 파일을 참조할 수 있습니다.

class GalleryLayout(ui.LayoutView):
    # 파일 객체 준비 (전송 시 files 파라미터에 포함되어야 함)
    # 주의: 레이아웃 정의 시점에는 파일이 업로드되지 않았으므로
    # 실제 전송 시점의 attachment 논리를 이해해야 함.

    gallery = ui.MediaGallery(
        # URL 기반 아이템
        discord.MediaGalleryItem("[https://example.com/image1.png](https://example.com/image1.png)", description="외부 이미지"),

        # 첨부 파일 기반 아이템 (스포일러 설정 가능)
        discord.MediaGalleryItem("attachment://local_image.jpg", spoiler=True)
    )

 

discord.File 객체를 MediaGalleryItem 생성자에 바로 전달할 수도 있지만, 메시지 전송 시 files=[...] 파라미터에는 여전히 포함시켜야 합니다.

6. LayoutView 마이그레이션과 예제

기존 ui.View 코드를 ui.LayoutView로 바꿀 때는 다음 단계를 따르면 됩니다.

  1. 상속 변경: class MyView(ui.View) -> class MyView(ui.LayoutView).
  2. 버튼/셀렉트 래핑: 최상위 레벨의 @ui.button 데코레이터를 제거하고, ui.ActionRow() 인스턴스를 생성한 후 @action_row_instance.button 형태로 변경합니다.
  3. 레이아웃 요소 추가: TextDisplay나 Separator를 추가하여 기존 Embed description에 의존하던 텍스트를 컴포넌트로 이관합니다.

6.1 종합 예제: 서버 관리 대시보드

다음은 V2의 주요 컴포넌트를 모두 사용한 대시보드 코드입니다.

import discord
from discord.ext import commands
from discord import ui

# 봇 인스턴스 설정
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)

class ServerControlPanel(ui.LayoutView):
    """
    서버 관리를 위한 Components V2 기반 대시보드 레이아웃
    """

    #  헤더 영역 (TextDisplay)
    header = ui.TextDisplay(
        "## 🛠️ 서버 제어 패널", 
        style=discord.TextStyle.paragraph
    )

    #  시각적 분리 (Separator)
    sep_header = ui.Separator(visible=True, spacing=discord.SeparatorSpacing.small)

    #  상태 모니터링 섹션 (Container + Section)
    class StatusContainer(ui.Container):
        def __init__(self):
            super().__init__(accent_color=0x5865F2) # Blurple Color

            self.cpu_section = ui.Section(
                "CPU 사용량",
                accessory=ui.Button(label="34%", disabled=True, style=discord.ButtonStyle.secondary)
            )
            self.ram_section = ui.Section(
                "RAM 사용량",
                accessory=ui.Button(label="12GB / 16GB", disabled=True, style=discord.ButtonStyle.secondary)
            )

    status_box = StatusContainer()

    #  여백 (Spacer)
    spacer = ui.Separator(visible=False, spacing=discord.SeparatorSpacing.large)

    #  관리 기능 선택 (ActionRow + Select)
    menu_row = ui.ActionRow()

    @menu_row.select(
        placeholder="수행할 작업을 선택하세요...",
        options=
    )
    async def action_select(self, interaction: discord.Interaction, select: ui.Select):
        selection = select.values
        # interaction handling logic
        await interaction.response.send_message(f"선택된 작업: {selection}", ephemeral=True)

    #  긴급 제어 (ActionRow + Button)
    control_row = ui.ActionRow()

    @control_row.button(label="서버 재시작", style=discord.ButtonStyle.danger)
    async def reboot(self, interaction: discord.Interaction, button: ui.Button):
        await interaction.response.send_message("서버 재시작 시퀀스를 시작합니다...", ephemeral=True)

    @control_row.button(label="새로고침", style=discord.ButtonStyle.secondary)
    async def refresh(self, interaction: discord.Interaction, button: ui.Button):
        # 뷰 업데이트 예시
        await interaction.response.edit_message(view=self)

@bot.command()
async def panel(ctx):
    """대시보드 출력 명령어"""
    view = ServerControlPanel()
    await ctx.send(view=view)

# 봇 실행 (토큰은 환경변수 등으로 관리 권장)
# bot.run("YOUR_TOKEN_HERE")

6.2 코드 포인트

  1. 중첩 클래스: StatusContainer를 내부나 외부에 정의해서 모듈화된 UI를 만들 수 있습니다.
  2. 비활성 버튼: disabled=True인 버튼은 뱃지나 상태 표시기처럼 쓸 수 있습니다.
  3. 레이아웃 흐름: 헤더 -> 구분선 -> 컨테이너 -> 공백 -> 셀렉트 -> 버튼 순으로 수직 배치됩니다.

7. 성능 고려사항

Components V2 사용 시 주의할 점:

  • Payload Size: LayoutView는 기존 View보다 복잡한 JSON을 생성합니다. 중첩된 컨테이너와 많은 TextDisplay는 API 요청 크기를 키우므로, 40개 제한 전에 페이로드 크기 제한에 걸릴 수 있습니다.3
  • Rendering Cost: V2 컴포넌트는 렌더링 비용이 더 높습니다. MediaGallery나 복잡한 Container 중첩은 저사양 모바일에서 느려질 수 있습니다.
  • Interaction Latency: LayoutView는 상태가 없어서 상호작용 시 오버헤드는 적지만, find_item을 너무 많이 쓰지 않는 게 좋습니다.

'봇 개발 팁 > Discord.py' 카테고리의 다른 글

Discord.py로 디스코드 음악 봇 만들기: 디스호스트로 24시간 호스팅까지!  (0) 2025.06.04
'봇 개발 팁/Discord.py' 카테고리의 다른 글
  • Discord.py로 디스코드 음악 봇 만들기: 디스호스트로 24시간 호스팅까지!
디스호스트
디스호스트
쉽고 안정적인 디스코드 봇 호스팅 서비스, 디스호스트의 기술 블로그입니다. 디스호스트는 24시간 구동되는 서버를 통해 디스코드 봇을 대신 구동시켜 드리는 서비스를 제공하고 있습니다.
  • 디스호스트
    디스호스트 기술 블로그
    디스호스트
  • 블로그 메뉴

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

    • 디스호스트
  • hELLO· Designed By정상우.v4.10.3
디스호스트
Discord.py Components V2 사용 가이드 (신형 임베드, 줄 나누기, 임베드 내에 버튼 등)
상단으로

티스토리툴바