Chat | CoUI
본문으로 건너뛰기
LogoCoUI

Chat

통일된 채팅 메시지 행 / 말풍선 / 그룹 컴포넌트.

목차

Chat / ChatGroup / ChatBubble 은 Flutter / Web 양쪽에서 동일한 API 로 채팅 인터페이스를 구성합니다.

기본 사용법 (Basic Usage)#

Chat(
  avatar: Avatar(initials: 'S'),
  header: Text('Support'),
  bubble: ChatBubble(child: Text('Hello! How can I help?')),
  footer: Text('10:00 AM'),
)

// 오른쪽(end)이면 primary 말풍선 · 꼬리 end 가 기본입니다.
Chat(
  alignment: CoreChatAlignment.end,
  bubble: ChatBubble(child: Text('I have a question.')),
  footer: Text('10:01 AM'),
)
Chat(
  avatar: Avatar(initials: 'S'),
  header: Text('Support'),
  bubble: ChatBubble(
    child: Text('Hello! How can I help?'),
  ),
  footer: Text('10:00 AM'),
)

// 오른쪽(end)이면 primary 말풍선 · 꼬리 end 가 기본입니다.
Chat(
  alignment: CoreChatAlignment.end,
  bubble: ChatBubble(child: Text('I have a question.')),
  footer: Text('10:01 AM'),
)

빠른 오버라이드 (Chain)#

이미 만든 Chat 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class ChatChainExample extends StatelessWidget {
  const ChatChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const Chat(
          avatar: Avatar(initials: 'S'),
          header: Text('Support'),
          bubble: ChatBubble(
            child: Text('Hello! How can I help?'),
          ),
          footer: Text('10:00 AM'),
        ).withStyle(
          const CoreChatStyle(
            avatarSpacing: CoreSpace.space16,
            groupSpacing: CoreSpace.space4,
            headerTextStyle: CoreTextStyle.token(
              CoreTextStyles.labelLarge,
              color: CoreColor.token(CoreColors.primary),
            ),
            footerTextStyle: CoreTextStyle.token(
              CoreTextStyles.labelSmall,
              color: CoreColor.token(CoreColors.onSurfaceVariant),
            ),
          ),
        ),
        const Gap.space12(),
        const Chat(
          alignment: CoreChatAlignment.end,
          bubble: ChatBubble(
            child: Text('I have a question.'),
          ),
          footer: Text('10:01 AM'),
        ),
      ],
    );
  }
}
class ChatChainExample extends StatelessComponent {
  const ChatChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        Chat(
          avatar: Avatar(initials: 'S'),
          header: const Text('Support'),
          bubble: const ChatBubble(
            child: Text('Hello! How can I help?'),
          ),
          footer: const Text('10:00 AM'),
        ).withStyle(
          const CoreChatStyle(
            avatarSpacing: CoreSpace.space16,
            groupSpacing: CoreSpace.space4,
            headerTextStyle: CoreTextStyle.token(
              CoreTextStyles.labelLarge,
              color: CoreColor.token(CoreColors.primary),
            ),
            footerTextStyle: CoreTextStyle.token(
              CoreTextStyles.labelSmall,
              color: CoreColor.token(CoreColors.onSurfaceVariant),
            ),
          ),
        ),
        const Gap.space12(),
        const Chat(
          alignment: CoreChatAlignment.end,
          bubble: ChatBubble(
            child: Text('I have a question.'),
          ),
          footer: Text('10:01 AM'),
        ),
      ],
      classes: 'flex flex-col w-full',
    );
  }
}

Live Preview#

컴포넌트 구성#

  • Chat — 한 번의 메시지 한 줄. 슬롯: avatar / header / bubble / footer.
  • ChatGroup — 같은 발신자의 메시지 묶음. 슬롯: avatar / header / bubbles[] / footer.
  • ChatBubble — 말풍선 컨테이너. 슬롯: child. variant (9 종) 와 tail boolean 으로 색상 / 꼬리 제어. 꼬리가 놓이는 변(tailEdge, 기본 아래)과 꼬리 없는 말풍선의 꼬리 쪽 모서리(tailCornerRadius)는 CoreChatBubbleStyle 이 정합니다.

Chat 은 자기 alignment 를 안에 든 ChatBubble 에 넘기고, ChatBubble 은 alignment · variant · tail 을 적지 않으면 그 정렬이 정하는 기본값을 씁니다. 보낸 메시지(end)는 primary 말풍선에 꼬리가 끝 쪽으로, 받은 메시지(start)는 neutral 말풍선에 꼬리 없음입니다. 직접 적은 값은 그대로 지킵니다.

사용 시기 (When to Use)#

발신자별 정렬과 말풍선 색상(필요하면 꼬리)을 갖는 메시지 행이면 Chat / ChatBubble, 같은 발신자의 연속 메시지를 한 아바타로 묶으면 ChatGroup 을 씁니다. 말풍선 없이 일반 행·열 목록만 필요하면 Listing 입니다. 메시지 전송·구독 기능의 직접 대체 컴포넌트는 없습니다.

Props / Parameters#

W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W? 는 선택 슬롯, List<W> 는 콘텐츠 목록입니다.

Chat#

속성타입기본값설명
bubble W 필수 메시지 본체 (보통 ChatBubble).
avatar W? null 행 옆에 표시되는 아바타.
header W? null 버블 위 헤더 (발신자명 등).
footer W? null 버블 아래 풋터 (타임스탬프).
alignment CoreChatAlignment start start / end . 안에 든 bubble 에도 넘어가 말풍선의 기본 alignment · variant · tail 을 정합니다.
chatStyle CoreChatStyle? null gap / header & footer 텍스트 스타일 override.

ChatBubble#

속성타입기본값설명
childW필수메시지 내용.
variant CoreChatBubbleVariant? null neutral / plain / primary / secondary / accent / info / success / warning / error. null 이면 말풍선의 정렬이 정합니다 — end 는 primary , start 는 neutral . plain 은 면이 없는 말(투명 면 · onSurfaceVariant 글 · 여백 0 · 폭 전체)입니다.
alignment CoreChatAlignment? null 꼬리 쪽. 꼬리와 꼬리 쪽 모서리가 이쪽(시작 = 왼쪽, 끝 = 오른쪽)에 놓입니다. null 이면 안에 든 Chat 행의 정렬을, 행 밖이면 start 를 씁니다.
tail bool? null 꼬리(speech-bubble pointer) 표시. null 이면 말풍선의 정렬이 정합니다 — end 는 꼬리 있음, start 는 꼬리 없음. 채움이 없는 말풍선( plain , 투명 override)은 true 여도 꼬리를 그리지 않습니다.
chatBubbleStyle CoreChatBubbleStyle? null padding / radius / 색상 / maxWidthFactor 등 override. 우선순위는 chatBubbleStyle > 변형 기본값 > 공통 기본값입니다.

ChatGroup#

속성타입기본값설명
bubblesList<W>필수같은 발신자의 연속 메시지들.
avatar W? null 그룹에 한 번만 표시되는 아바타.
headerW?null그룹 헤더.
footerW?null그룹 풋터.
alignment CoreChatAlignment start 그룹 정렬.
chatStyle CoreChatStyle? null gap / 텍스트 스타일 override.

CoreChatStyle 필드#

필드타입설명
avatarSpacing double? Padding spacing between the avatar and the message column (logical px). Fed into the avatar wrapper's EdgeInsetsDirectional (Flutter) / margin-inline-* (Web) — not a Gap , so the value stays a raw double.
groupSpacing double? Inter-bubble spacing between consecutive bubbles inside a ChatGroup (logical px, pre-scaling). Forwarded directly to Column.spacing (Flutter) / flex gap CSS (Web) — both are native paint-API arguments that consume raw doubles, so this slot stays scalar ( double ).
headerTextStyle CoreTextStyle? Optional text style for the header slot (sender name).
footerTextStyle CoreTextStyle? Optional text style for the footer slot (timestamp / status).

CoreChatStyle 변형별 기본값 (CoreChatBubbleVariantStyle)#

필드 neutral primary secondary accent info success warning error plain
backgroundColor surfaceContainerHigh primary secondary tertiary info success warning error transparent
foregroundColor onSurface onPrimary onSecondary onTertiary onInfo onSuccess onWarning onError onSurfaceVariant
padding — — — — — — — — zero
maxWidthFactor — — — — — — — — 1

동작 스펙 (Behavior)#

  • Chat 은 header → bubble → footer를 세로로 쌓고 아바타는 alignment의 시작/끝 쪽에 한 번 배치합니다. ChatGroup 은 같은 구조 안에 bubbles 를 주어진 순서로 쌓습니다.
  • ChatBubble.tail 은 말풍선 꼬리 표시만 제어합니다. 적지 않으면 정렬이 정합니다 — start 는 꼬리 없음, end 는 꼬리 있음. alignment 는 꼬리 방향, variant 는 색상, maxWidthFactor 는 부모 너비 대비 최대 비율(기본 75%, plain 은 100%)이며 꼬리가 없을 때도 양 플랫폼에서 똑같이 적용됩니다.
  • 말풍선이 쓰는 정렬은 직접 적은 alignment, 없으면 안에 든 Chat 행의 alignment, 행 밖이면 start 입니다. variant · tail 의 기본값은 이렇게 정해진 그 정렬에서 나옵니다 — 행은 end 인데 말풍선이 alignment: start 를 직접 적었다면 그 말풍선은 start 쪽 기본값(neutral · 꼬리 없음)을 씁니다. 행의 header · footer · avatar 칸에 든 말풍선은 행 안의 말풍선이 아니라서 정렬을 받지 않습니다.
  • 팝오버 · 대화상자처럼 행 밖으로 띄운 화면 안의 말풍선도 행의 정렬을 받지 않습니다. ChatGroup 도 정렬을 안의 bubbles 에 넘기지 않으므로, 그룹 안의 말풍선은 alignment · variant · tail 을 각자 적습니다.
  • 꼬리는 말풍선의 채움을 이어 그리는 모양이라 채움이 없으면 그릴 것이 없습니다. plain 이나 투명으로 덮어쓴 말풍선은 tail: true 여도 꼬리 없이 그립니다.
  • 사용자 말은 Chat(alignment: end) 만으로 primary 말풍선이 되고, AI 답은 plain + start 로 짜면 보내는 쪽만 말풍선 면이 있고 받는 쪽은 면 없이 읽힙니다.
  • 꼬리는 CoreChatBubbleStyle.tailEdge 가 정하는 변(bottom 기본 · top)의 정렬 쪽 모서리에서 나갑니다. top 은 메시지가 시작하는 쪽, 행이 위에 맞추는 아바타 쪽을 가리킵니다. 꼬리를 그리면 그 모서리는 각지게 됩니다 — 꼬리의 바깥 변이 말풍선의 옆선을 이어야 하기 때문입니다.
  • 꼬리 없는 말풍선은 CoreChatBubbleStyle.tailCornerRadius 로 꼬리 쪽 모서리 하나만 다르게 둘 수 있습니다(보낸 쪽 모서리만 덜 둥근 모양). 값이 없으면 그 모서리는 borderRadius 의 모서리 그대로라, 반경을 다시 정하면 네 모서리가 함께 움직입니다. 꼬리를 그리면 이 값과 상관없이 각집니다. 밀도 · 표면 스타일은 이 모서리를 나머지 셋과 같이 움직입니다.
  • 꼬리와 꼬리 쪽 모서리는 물리 방향(시작 = 왼쪽, 끝 = 오른쪽)입니다 — CoreBorderRadius 가 물리 네 모서리인 것과 같은 도달 범위라, 오른쪽에서 왼쪽으로 쓰는 페이지에서 뒤집히지 않습니다.
  • 모두 표시용 구성 요소이며 메시지 전송 · 수신 · 자동 스크롤 · 읽음 처리 · 콜백 · 비활성 상태를 제공하지 않습니다. 내용 갱신과 목록 컨테이너는 호출부가 관리합니다.
// 앱 전체: 보낸 쪽 위 모서리만 6 — 내 말풍선 · 상대 말풍선 모두 테마 하나로
CoreComponentTheme(
  chatBubble: CoreChatBubbleTheme(
    style: CoreChatBubbleStyle(
      tailEdge: CoreChatBubbleTailEdge.top,
      tailCornerRadius: CoreRadius.radius6,
    ),
  ),
)

사용 가이드라인 (Usage Guidelines)#

✅ Do#

사용자 말은 end(기본 primary), AI 답은 plain + start

Chat(
  alignment: CoreChatAlignment.end,
  bubble: ChatBubble(child: Text('Summarize this document.')),
)

Chat(
  bubble: ChatBubble(
    variant: CoreChatBubbleVariant.plain,
    child: Text('Here is a short summary of the document.'),
  ),
)

보내는 쪽에만 말풍선 면을 주고 받는 쪽은 면 없는 글로 두면 긴 답이 화면 폭을 따라 편하게 읽힙니다. 보낸 쪽(end) 말풍선은 꼬리가 기본으로 켜지고, 받는 쪽(start)은 꺼져 있습니다 — 보낸 쪽 꼬리를 끄려면 ChatBubble(tail: false, ...), 받는 쪽에 꼬리를 켜려면 ChatBubble(tail: true, ...) 로 직접 적습니다.

정렬은 Chat 에만 적기

Chat(
  alignment: CoreChatAlignment.end,
  bubble: ChatBubble(child: Text('I have a question.')),
)

Chat.alignment 는 행 레이아웃(아바타 위치·정렬)을 정하고, 안에 든 ChatBubble 에도 넘어가 말풍선의 정렬(꼬리 방향까지)과 기본 variant · tail 을 정합니다. 그래서 같은 값을 말풍선에 또 적을 필요가 없습니다. 말풍선에 alignment · variant · tail 을 직접 적으면 그 값이 이깁니다 — 다만 행 밖에서 쓰는 말풍선은 행의 정렬을 받지 못하므로 ChatBubble(alignment: ...) 로 직접 적어야 합니다.


❌ Don't#

발신자 구분을 정렬/색상에만 맡기지 않기

// ❌ alignment(start/end)와 variant 색만으로 발신자를 구분
Chat(bubble: ChatBubble(child: Text(message)))

// ✅ header 슬롯에 발신자명을 텍스트로 함께 제공
Chat(
  header: Text(senderName),
  bubble: ChatBubble(child: Text(message)),
)

Chat / ChatBubble / ChatGroup은 role 이나 Semantics를 emit 하지 않는 순수 레이아웃 래퍼입니다. 발신자 구분이 정렬과 배경색이라는 시각적 신호로만 이루어지면 스크린 리더 사용자는 누가 보낸 메시지인지 알 수 없으므로, header 슬롯으로 텍스트 정보를 함께 줘야 합니다.

접근성 (Accessibility)#

역할#

Chat · ChatBubble · ChatGroup 세 클래스 모두 양 플랫폼에서 role 이나 Semantics 를 하나도 emit 하지 않습니다. Flutter 는 Column / Row / Padding / DefaultTextStyle(버블은 DecoratedBox / Stacks / CustomPaint) 만 조립하고, Web 은 세 클래스 모두 루트가 <div> 이며 caller attributes 만 통과시킵니다 — 네이티브 시맨틱 요소도 쓰지 않습니다.

즉 이들은 레이아웃 래퍼이며, 보조 기술이 받는 것은 avatar / header / bubble / footer 슬롯에 넣은 자식이 스스로 알리는 내용이 전부입니다. 대화라는 구조를 알리는 일은 소비자 몫입니다.

키보드#

양 플랫폼 모두 키를 처리하지 않습니다.

포커스 관리#

포커스 개념이 없습니다 — FocusNode · Focus · tabindex · 포커스 스타일이 어디에도 없습니다. Chat / ChatGroup / ChatBubble 자체는 포커스를 받지 않으며, 포커스 가능한 것은 슬롯에 넣은 자식뿐입니다.

스크린 리더#

슬롯 내용을 위젯 / DOM 순서대로 이어진 평범한 텍스트로 읽습니다. 메시지라는 사실도, 발신자가 누구인지도, 어느 쪽에서 온 메시지인지도 알리지 않습니다 — 발신자 구분은 alignment(start/end 레이아웃) 와 CoreChatBubbleVariant 배경색, 즉 순수하게 시각적인 신호만으로 이루어집니다.

버블 꼬리는 장식이며 조용합니다(Flutter 는 CustomPaint, Web 은 빈 <div>) — 이건 의도한 동작입니다.

알려진 제약#

아래는 전부 양 플랫폼에 동일하게 존재합니다.

  • role="log" / aria-live 가 없어 진행 중인 대화에 도착한 새 메시지는 안내되지 않습니다. 실시간 대화라면 메시지 목록을 감싸는 live region 을 소비자가 직접 만들어야 합니다.
  • list / listitem 구조가 없어 ChatGroup 의 연속 버블은 서로 구분되지 않는 형제로 읽힙니다.
  • header(발신자) 와 footer(타임스탬프) 는 해당 버블과 aria-labelledby / Semantics 로 연결되어 있지 않은 별개 텍스트입니다 — 읽히기는 하지만 어느 메시지에 속하는지는 전달되지 않습니다.
  • Web ChatBubble 은 <article> 도, role 도, 접근 가능한 이름도 emit 하지 않습니다.
  • 발신자 구분이 정렬과 색에만 실려 있으므로, 누가 보낸 메시지인지는 텍스트로도 제공해야 합니다.

모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
행·그룹Row / Column.spacingflex 행·열과 CSS gap
꼬리 CustomPaint 경로(위쪽 꼬리는 세로로 뒤집은 경로) CSS 꼬리 엘리먼트 — 같은 경로의 clip-path, 위쪽 꼬리는 top: 0
최대 너비LayoutBuilder 의 부모 제약 × factorCSS 최대 너비 백분율
의미·이벤트자식 의미가 통과루트 attributes / 이벤트를 호출부에서 제공 가능

관련 컴포넌트#

  • Avatar — 메시지 발신자 표시.
  • Loading — AI 응답 대기 인디케이터.
  • TextField — 입력 영역.