reopt design
DocsExploreTemplatesCanvasToolsPricing
Login
Start
Overview
Start
Playground
Core Concepts
Component Guide
Rich Parts
Styling
Build & Operate
Tool Approval UI
Custom Parts
Production readiness
Oopt-chat
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • About
  • Contact
  • Terms of Service
  • Privacy Policy

reopt Inc.Business registration no. 217-88-02453contact@reopt.ai

© 2026 reopt Inc. All rights reserved.

Build & Operate
  1. Docs
  2. /
  3. Build & Operate
  4. /
  5. Custom Part Renderer

커스텀 파트

MessageParts의 renderPart 콜백을 사용하여 기본 제공되지 않는 메시지 파트 타입에 대한 맞춤 렌더러를 작성하는 방법을 설명합니다.

reopt design · Updated Nov 1, 2026

1. renderPart API

MessageParts의 renderPart 콜백은 각 파트에 대해 호출됩니다. undefined를 반환하면 기본 렌더러가 사용되고, ReactNode를 반환하면 해당 렌더러를 완전히 대체합니다.

tsx
import type { UIMessage } from "ai";
import { MessageParts, createPartRenderer, ExecutionPlan, type ExecutionStep } from "@reopt-ai/opt-chat";

type AppMessage = UIMessage<unknown, {
  plan: { steps: ExecutionStep[] };
}>;

const renderPart = createPartRenderer<AppMessage>({
  "data-plan": (part, index, context) => (
    <ExecutionPlan title="실행 계획" steps={part.data.steps} />
  ),
});

// undefined: 기본 렌더러, null: 숨기기, ReactNode: 대체하기
<MessageParts<AppMessage>
  parts={message.parts}
  variant="compact"
  isStreaming={session.isStreaming}
  onToolApproval={(id, approved) => session.addToolApprovalResponse({ id, approved })}
  renderPart={renderPart}
/>

2. 예제: 차트 파트 렌더러

AI 응답에 차트 데이터가 포함된 경우, 커스텀 "data-chart" 파트 렌더러를 만들어 opt-charts의 BarChart로 시각화하는 전체 예제입니다.

tsx
"use client";

import { useMemo } from "react";
import { DefaultChatTransport, type UIMessage } from "ai";
import {
  useChatSession, MessageParts, createPartRenderer,
  Conversation, ConversationContent, Message, MessageContent,
  PromptInput, PromptInputTextarea, PromptInputFooter, PromptInputSubmit,
} from "@reopt-ai/opt-chat";
import { BarChart } from "@reopt-ai/opt-charts";

type AppMessage = UIMessage<unknown, {
  chart: { title: string; rows: Array<{ name: string; value: number }> };
}>;

const renderPart = createPartRenderer<AppMessage>({
  "data-chart": ({ data }) => (
    <section aria-label={data.title}>
      <h4>{data.title}</h4>
      <BarChart data={data.rows} series={[{ dataKey: "value", name: "값" }]} height={200} />
    </section>
  ),
});

export default function ChatWithCustomParts() {
  const transport = useMemo(() => new DefaultChatTransport<AppMessage>({ api: "/api/chat" }), []);
  const session = useChatSession<AppMessage>({ transport });
  return (
    <div className="flex h-dvh flex-col">
      <Conversation>
        <ConversationContent>
          {session.messages.map((msg, index) => (
            <Message key={msg.id} from={msg.role}>
              <MessageContent>
                <MessageParts<AppMessage> parts={msg.parts} renderPart={renderPart}
                  isStreaming={index === session.messages.length - 1 && session.isStreaming} />
              </MessageContent>
            </Message>
          ))}
        </ConversationContent>
      </Conversation>
      <PromptInput onSubmit={session.handleSubmit} isLoading={session.isLoading} onStop={session.stop}>
        <PromptInputTextarea />
        <PromptInputFooter><PromptInputSubmit /></PromptInputFooter>
      </PromptInput>
    </div>
  );
}

3. 여러 커스텀 파트 등록

커스텀 파트가 여러 개인 경우, 레지스트리 패턴으로 renderPart 콜백을 깔끔하게 관리할 수 있습니다.

tsx
import { createPartRenderer, findDataPart, ExecutionPlan, Terminal, type ExecutionStep } from "@reopt-ai/opt-chat";
import type { UIMessage } from "ai";

type AppMessage = UIMessage<unknown, {
  plan: { steps: ExecutionStep[] };
  terminal: { output: string; running: boolean };
}>;

const renderPart = createPartRenderer<AppMessage>({
  "data-plan": ({ data }) => <ExecutionPlan title="실행 계획" steps={data.steps} />,
  "data-terminal": ({ data }) => <Terminal variant="compact" output={data.output} isStreaming={data.running} />,
});

// 서버는 terminal snapshot의 id를 해당 toolCallId로 지정합니다.
const terminal = findDataPart<AppMessage, "data-terminal">(message.parts, "data-terminal", toolCallId);
// 동일 type + id로 보내면 AI SDK가 해당 snapshot을 갱신합니다.
// data payload는 서버/클라이언트의 AI SDK dataPartSchemas로 검증하세요.

4. 실행 상태와 복원 계약

preliminary 결과는 완료가 아닙니다. 스트림 연결이 끝나도 클라이언트 도구와 백그라운드 작업은 결과 대기로 남습니다. 실행이 실제로 중단되었음을 아는 경우에만 interrupted를 지정하고, 별도 작업 상태는 executionStatus로 전달합니다. AgentRun은 검증된 하위 에이전트 메시지를 표시하고, Questionnaire는 단일·복수 선택과 자유 입력 응답을 Promise 기반 콜백으로 제출합니다.

restoreMessagesAt은 대화만 잘라냅니다. 파일 변경이나 외부 도구 실행을 되돌리지 않으므로 실제 복원은 애플리케이션의 서버 체크포인트에 연결해야 합니다. Sources의 sourceId와 인용에 사용하는 번호의 매핑은 서버가 안정적으로 유지하세요. URL 출처는 중복을 제거하며, renderPart가 반환한 출처는 기본 목록에 다시 넣지 않습니다.

실행 상태 재생 예제
PreviousTool Approval UIAI SDK 도구 호출 승인/거부 흐름과 Confirmation 컴포넌트 사용법Build & Operate
Go to Tool Approval UI
NextProduction readinessinstall smoke, browser matrix, streaming soak, release ops를 묶은 opt-chat 배포 직전 체크리스트Build & Operate