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 에 없음)이 맞다.
컴포넌트
RemovableTabBar— 닫기 가능한 세션 탭 스위처ChatUserMessage— 사용자 발화 (bubble + 첨부)ChatAssistantMessage— 에이전트 응답 (배경 없는 plain text + markdown)ChatComposer— 입력 영역 (toolbar / accessory 슬롯)IconButton— "새 채팅" 등 탭바 옆 액션