Core Concepts
핵심 컴포넌트
opt-chat의 core 모듈을 구성하는 컴포넌트와 훅의 역할, props, 조합 패턴을 설명합니다.
reopt designUpdated
1. Core 모듈 개요
Core 모듈은 채팅 UI의 뼈대를 담당합니다. * 표시는 필수 컴포넌트를 나타냅니다.
| 컴포넌트 | 설명 | 주요 Props |
|---|---|---|
| Conversation* | 채팅 로그의 스크롤 컨테이너 + 엔진. 하단 팔로우, 턴 앵커링, prepend 위치 보존을 자체 구현합니다 (외부 스크롤 라이브러리 없음). | children, autoScroll, scrollEdgeThreshold, scrollPreviousItemPeek, scrollMargin |
| ConversationMessage | 메시지 턴 래퍼. scrollAnchor=true인 턴은 추가 시 하단 점프 대신 뷰포트 상단에 정착합니다 (보통 user 턴에 지정). content-visibility 컨테인먼트도 제공합니다. | messageId, scrollAnchor, children |
| ConversationScrollButton | 하단에 안 본 메시지가 있을 때 나타나는 '아래로 스크롤' 버튼. 하단에 있을 때는 inert로 tab 순서에서 빠집니다. | scrollLabel, className |
| Message* | 단일 메시지 렌더러. role에 따라 사용자/어시스턴트 버블을 자동 구분합니다. | from, isStreaming, children |
| PromptInput* | 메시지 입력 영역. 자동 리사이즈, 파일 첨부, 전역 드롭, 모델 선택을 지원합니다. | onSubmit, accept, maxFiles, globalDrop, children |
| PromptInputAttachments | PromptInput이 관리하는 첨부 목록을 렌더링하고 제거 액션을 연결합니다. | variant, labels, onRemove |
| MessageParts | AI SDK UIMessage.parts 배열을 파트 타입별로 자동 매핑하여 렌더링합니다. | parts, renderPart |
| Shimmer | 스트리밍 중 표시되는 타이핑 애니메이션 컴포넌트. | className |
| Suggestion* | 추천 프롬프트 버튼. 클릭 시 자동으로 메시지를 전송합니다. | prompt, label, onClick |
| useChatSession | AI SDK useChat를 감싸는 훅. PromptInput 첨부 payload를 files 전송으로 연결합니다. | transport, messages, onToolCall |
2. Compound Component 패턴
opt-chat은 compound component 패턴을 사용합니다. Conversation이 세션 컨텍스트를 제공하고, Message와 PromptInput이 이를 소비합니다. children을 통해 각 단계에서 커스텀 렌더링을 삽입할 수 있습니다.
tsx
// Conversation > ConversationMessage > Message > MessageParts 계층 구조
<Conversation>
<ConversationContent>
{session.messages.map((msg) => (
// scrollAnchor=user 턴 → 전송 시 질문이 상단에 정착
<ConversationMessage
key={msg.id}
messageId={msg.id}
scrollAnchor={msg.role === "user"}
>
<Message from={msg.role}>
<MessageContent>
<MessageParts parts={msg.parts} isStreaming={session.isStreaming} />
</MessageContent>
</Message>
</ConversationMessage>
))}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput onSubmit={session.handleSubmit}>
<PromptInputAttachments />
<PromptInputTextarea />
<PromptInputFooter>
<PromptInputActionAddAttachment />
<PromptInputSubmit />
</PromptInputFooter>
</PromptInput>3. MessageParts 자동 매핑
Message 컴포넌트는 내부적으로 MessageParts를 사용하여 메시지의 content 배열을 파트 타입별로 자동 렌더링합니다. 기본 매핑 규칙과 커스텀 renderPart 콜백을 설명합니다.
tsx
// MessageParts의 자동 매핑 규칙
// AI SDK UIMessage.parts 배열의 type에 따라 렌더러가 결정됩니다:
// type: "text" → MessageResponse (마크다운 렌더링)
// type: "reasoning" → Reasoning (사고 과정 접기/펼치기)
// type: "tool-*" → Tool (도구 호출 + 상태 배지)
// type: "source-url" → Sources (접을 수 있는 출처 목록)
// type: "file" → ChatImage (image/*) 또는 null
// 커스텀 파트 타입은 renderPart 콜백으로 처리합니다
<MessageParts
parts={message.parts}
renderPart={(part) => {
if (part.type === "chart") return <MyChart data={part.data} />;
return null; // null 반환 시 기본 렌더러 사용
}}
/>4. 스크롤 엔진과 턴 앵커링
Conversation은 외부 스크롤 라이브러리 없이 자체 스크롤 엔진을 내장합니다. 세 가지 동작이 자동으로 맞물립니다.
- 하단 팔로우 — 독자가 하단에 있을 때만 스트리밍을 따라 스크롤합니다. 위로 스크롤하면 팔로우가 해제되고 읽던 위치가 보존됩니다. 하단으로 돌아오면 다시 팔로우가 재개됩니다.
- 턴 앵커링 —
ConversationMessage에scrollAnchor를 지정한 턴(보통 user 메시지)은 추가될 때 하단으로 튀는 대신 뷰포트 상단 근처(scrollPreviousItemPeek, 기본 64px 아래)에 정착합니다. 이전 대화가 살짝 보여 문맥이 유지되고, 응답이 스트리밍 으로 자라도 앵커 위치가 고정됩니다. - prepend 위치 보존 — 히스토리 페이지네이션으로 위쪽에 메시지가 추가돼도 현재 스크롤 위치가 유지됩니다.
접근성: 뷰포트는 role="region" + tabIndex=0(키보드 스크롤)이고, 메시지 목록은 role="log" + aria-relevant="additions"로 신규 메시지를 스크린리더에 알립니다. 프로그래매틱 스크롤이 필요하면 useConversationScroller()(scrollToEnd/scrollToStart/ scrollToMessage), 스크롤 상태 구독은 useConversationScrollable()를 사용합니다.
tsx
// 프로그래매틱 스크롤 — 전송 직후 하단으로 이동
import { useConversationScroller } from "@reopt-ai/opt-chat";
function SendButton() {
const { scrollToEnd, scrollToMessage } = useConversationScroller();
// scrollToEnd({ behavior: "smooth" })
// scrollToMessage(messageId, { behavior: "smooth" })
}
// 스크롤 옵션 튜닝 (기본값 표시)
<Conversation
autoScroll // 하단에 있을 때 스트리밍 팔로우
scrollEdgeThreshold={8} // 상/하단 판정 여유(px)
scrollPreviousItemPeek={64} // 앵커 위로 보여줄 이전 대화(px)
>
...
</Conversation>