Pluto Design System
Patterns

Chat

여러 채팅 세션을 한 화면에서 오가며 메시지를 주고받는 채팅 영역. RemovableTabBar + 메시지 스레드 + ChatComposer 를 한 컨테이너에 조립하는 권장 형태.

상황

사용자가 여러 대화 세션을 동시에 열어두고 탭으로 전환하면서, 각 세션에서 메시지를 주고받아야 할 때. 단발 Q&A 가 아니라 세션이 누적되는 환경(브라우저 탭처럼 채팅을 다중으로 열어두는 데스크탑 앱)이 전제다.

대화가 단일 스레드 하나로 끝나거나, 세션 전환이 사이드바 리스트로 이뤄지면 이 패턴이 아니다.

권장 조합

상단에 RemovableTabBar(세션 스위처) + 가운데에 스크롤되는 메시지 스레드(ChatUserMessage / ChatAssistantMessage) + 하단에 ChatComposer 를 한 컨테이너 안에 쌓는 구조.

전체 화면에서 보기RemovableTabBar + ChatUserMessage + ChatAssistantMessage + ChatComposer 조립을 풀 viewport 로 띄운다.
<div className="flex flex-col h-full">
  <div className="flex items-center gap-[8px] px-[12px] py-[8px]">
    <RemovableTabBar
      tabs={tabs}
      activeId={activeId}
      onSwitch={setActiveId}
      onClose={closeTab}
    />
    <IconButton size="sm" variant="subtle" aria-label="새 채팅" onClick={addTab}>
      <Plus />
    </IconButton>
  </div>

  <div className="flex-1 overflow-y-auto">
    <div className="mx-auto flex max-w-[680px] flex-col gap-[20px] px-[20px] py-[24px]">
      {messages.map((m) =>
        m.role === "user" ? (
          <ChatUserMessage key={m.id} content={m.text} />
        ) : (
          <ChatAssistantMessage key={m.id} content={m.text} renderMarkdown={renderMarkdown} />
        ),
      )}
    </div>
  </div>

  <div className="px-[16px] py-[12px]">
    <ChatComposer
      value={draft}
      onChange={setDraft}
      onSubmit={submit}
      placeholder="메시지를 입력하세요…"
    />
  </div>
</div>

결정 근거

  • 컴포넌트가 아니라 패턴. 채팅 영역의 재사용 단위RemovableTabBar / ChatComposer / ChatUserMessage / ChatAssistantMessage 처럼 모양이 정해진 빌딩 블록이다. 반면 "채팅 영역 전체"는 탭별 상태를 어디에 두고, draft 를 어떻게 분리하고, 어떤 toolbar 가 붙는지 가 제품 결정이라 prop API 로 흡수하면 비대해진다.
  • 탭별 상태는 호출자 책임. 메시지 배열·composer draft 는 세션 단위로 분리되어야 하지만, 어디에 저장할지(메모리/IndexedDB/서버)는 제품마다 다르다. 패턴은 "탭 전환 시 draft 도 같이 전환된다" 는 행동 규약만 제시하고, 저장은 호출자가 정한다.
  • "새 탭" 액션은 탭바 외부에. RemovableTabBar 자체에 add 버튼 슬롯을 두지 않는다. 탭바 옆의 IconButton 으로 두면 위치·아이콘·동작을 제품이 자유롭게 정할 수 있고, 탭바 컴포넌트는 세션 스위칭 책임만 진다.
  • 스레드는 자체 스크롤. 컨테이너 전체가 스크롤되면 composer 가 함께 밀려서 입력 위치가 사라진다. 가운데 스레드에 flex-1 overflow-y-auto 를 주고 composer 를 외곽 flex column 의 마지막 자식으로 두면, 탭바·composer 는 고정되고 메시지만 흐른다.
  • 빈 상태도 패턴 일부. 새 탭이거나 첫 메시지 전이면 "아래에서 첫 메시지를 보내세요" 같은 짧은 안내 한 줄을 둔다. composer 자체가 화면에 항상 떠 있으므로 큰 일러스트나 유도 카드는 필요 없다.

거절 사례

  • 단일 스레드 채팅 — 한 화면에 대화 세션이 하나뿐이면 RemovableTabBar 가 들어갈 자리가 없다. 메시지 스레드 + composer 만 쓰는 단순 조립이지 패턴화할 만한 결정 지점이 없다.
  • 사이드바 기반 세션 리스트 — 세션 전환을 좌측 사이드바 항목으로 처리하면 SidebarList + 단일 스레드 조합이지 이 패턴이 아니다.
  • 모달성 채팅 패널 — 우하단에 떴다 사라지는 supports chat 위젯이면 다른 컨테이너 패턴(아직 PDS 에 없음)이 맞다.

컴포넌트