Skip to main content

Open UI (React) + Langgraph

구축 레이어​

  1. 컴포넌트 & 상태/액션 규칙
  • 열 지향 테이블: Table은 행(Row) 단위가 아닌 Col 단위의 열 중심 배열 구조 사용 (차트와의 데이터 연동 용이).
  • 폼(Form) 규칙: Form 중첩 금지, 스트리밍 지연 방지를 위해 각 FormControl을 개별 변수로 분리, 명시적 Buttons 필수.
  • 반응형 상태 & 액션: $variable 양방향 바인딩, 컬렉션 연산(@Filter, @Sort, @Count), 이벤트 트리거(@ToAssistant, @OpenUrl) 지원.
  • 카드 전용 인라인 요소 제약: Text, BoldText, IconText, MetricIndicatorInline 등은 카드 블록 내부에서만 사용 가능 (루트 직접 배치 불가).

UI 컴포넌트 카탈로그와 LangGraph 시스템 프롬프트 동기화 방법​

LLM이 생성할 OpenUI 컴포넌트를 추가하거나 이름·인자·사용 규칙을 변경하면, 프런트엔드 렌더링 카탈로그와 백엔드 시스템 프롬프트를 함께 갱신해야 한다.

현재 방식: 생성 후 명시적으로 복사

    1. 새 컴포넌트 구현과 명세를 OpenUI 라이브러리에 등록하고, 실제 렌더러와 frontend/src/library.ts의 생성용 export가 같은 카탈로그를 사용하도록 맞춘다.
    1. frontend에서 프롬프트를 생성하고 백엔드 자산으로 복사한다.
    1. 변경된 프롬프트에 새 컴포넌트의 이름·인자·규칙이 포함됐는지 확인한다. agent/graph.py에서 붙이는 에이전트 지시문은 별도로 유지한다.
[Frontend: 카탈로그 SSOT]
├── components (React UI 컴포넌트)
├── defineFunction() (사용자 정의 계산 함수)
└── defineAction() (사용자 정의 버튼 액션)
│
▼ (npm run generate)
[빌드 타임 프롬프트 생성 (@openuidev/cli)]
├── src/generated/spec.json (컴포넌트/함수 메타데이터 스펙)
└── src/generated/system-prompt.txt (순수 openui-lang 지침 텍스트)
│
▼ (명시적 파일 복사: cp)
[Python LangGraph 백엔드]
└── backend-langgraph/agent/system-prompt.txt
│ + 런타임 추가 지시문 (도구 설명, Mock 데이터 안내)
▼
[create_deep_agent()] ──► [LLM (OpenAI)]
│ (openui-lang 코드 생성)
▼
[브라우저 OpenUI 런타임]
├── React 컴포넌트 렌더링
└── 클라이언트 fn 로직 직접 실행

LangGraph JS → Python: 마이그레이션 주요 라이브러리​

openui/examples/agent-frameworks에서는 JS LangGraph를 사용했다. Python LangGraph로 옮길 때는 두 라이브러리의 Python 패키지가 없어 별도로 구현해야 한다.

JS 라이브러리원본 역할현재 Python 구현
@openuidev/langchainopenUIStreamTransformer로 LangGraph 이벤트를 OpenUI 스트림으로 변환OpenUIStreamTransformer로 직접 구현
@openuidev/lang-coregenerateSystemPrompt()로 컴포넌트 명세를 시스템 프롬프트로 변환컴포넌트 카탈로그 SSOT는 FE에 있고, FE에서 Autogen으로 생성한 system-prompt.txt를 읽어 사용
deepagentscreateDeepAgent()로 모델·도구·프롬프트를 결합한 에이전트 그래프 생성Python deepagents 패키지의 create_deep_agent() 사용

deepagents 패키지란?

  • LangChain·LangGraph 기반 deepagents는 LLM이 도구를 사용하며 여러 단계의 작업을 수행하도록 에이전트를 구성해 주는 라이브러리다.
  • https://github.com/langchain-ai/deepagents
  • create_react_agent와의 차이점 (langchain.agents.create_agent ) - 파일·컨텍스트 관리와 서브 에이전트 위임을 별도로 구성할 필요가 없다. deepagents가 상위 호환이다.
  • JS와 Python 버전 모두 LangChain 팀에서 관리하는 공식 에이전트 프레임워크다.

'데이터 조작 내장 함수: @Filter, @Sort, @Count, @Each 등' 확장 기능이 있는가?​

있다. defineFunction()으로 사용자 정의 함수를 만들고 라이브러리에 등록하면, OpenUI Lang에서 @함수명(...)으로 호출할 수 있다. 내장 함수 자체를 수정할 필요는 없다.

아래 코드는 설명용이다. (openui/packages/lang-core/src/functions.ts, library.ts, runtime/evaluator.ts)

정의와 등록​

import { defineFunction } from "@openuidev/react-lang";
import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib";
import { z } from "zod/v4";

const DiscountPrice = defineFunction({
name: "DiscountPrice", // OpenUI Lang에서 호출할 함수 이름
description: "가격에 할인율(0~100%)을 적용한 금액", // 모델에 전달할 함수 용도
params: z.object({ // 입력 스키마와 인자 순서
price: z.number(),
discountPercent: z.number().min(0).max(100),
}),
returns: z.number(),
// 함수 실제 구현체
fn: ({ price, discountPercent }) => price * (1 - discountPercent / 100),
});

export const library = openuiLibrary.extend({
functions: { add: [DiscountPrice] },
});
export const promptOptions = openuiPromptOptions;
  • 기존 라이브러리는 extend({ functions: { add: [...] } })로 확장할 수 있다.
  • 새 라이브러리를 구성할 때는 createLibrary({ components: [...], functions: [...] })에 등록
  • OpenUI Lang에서는 다음과 같이 계산값을 정의하여 UI에서 참조할 수 있다. 아래는 전체 UI 프로그램이 아닌 계산 부분이다.
$price = 10000
$discount = 20
discounted = @DiscountPrice($price, $discount)

params 객체의 키 순서가 위치 기반 인자 순서다. 위 호출은 fn({ price: 10000, discountPercent: 20 })로 실행되어 8000을 반환한다. 상태가 바뀌면 해당 상태를 참조하는 표현식이 다시 평가된다. 이 앱에서 계산은 브라우저의 OpenUI 런타임이 수행하며, 매번 LLM이나 Python 백엔드를 호출하지 않는다.

현재 Python 프로젝트에 적용할 때​

  • frontend/src/library.ts에서 확장한 라이브러리를 export한다
  • frontend에서 npm run generate를 실행
  • 프롬프트에는 함수의 사용 계약이 들어가고, 실행 구현인 fn은 프런트엔드에 남는다.

제약과 액션의 차이​

  • defineFunction()은 값을 계산하는 확장이다. 클릭 시 복사·외부 동작을 수행하는 확장은 defineAction()으로 정의하고 onAction에서 처리한다. 두 기능 모두 @이름(...) 문법을 쓰지만 실행 목적은 다르다.
  • 내장 함수나 기존 컴포넌트·함수·액션과 같은 이름은 등록할 수 없다. 예를 들어 Filter를 같은 이름으로 덮어쓰려 하면 라이브러리 생성 시 충돌 오류가 발생한다.
  • 함수 평가기는 fn을 동기로 호출한다. 계산용 함수는 부작용 없이 작성하는 것이 적합하며, 비동기 API 조회는 Query/tool 경로로 분리한다.
  • 인자·반환값 검증 실패 또는 함수 실행 중 예외 발생 -> 평가 결과 null로 처리

기본 제공 함수 전체와 조합 방법​

내장 함수는 14개, 내장 액션은 5개다. 아래는 언어 내장 기능 목록이다.

분류함수기능
필터@Filter(array, field, op, value)조건에 맞는 항목 추출
정렬@Sort(array, field, direction?)필드 기준 정렬. 기본 "asc", 역순 "desc"
집계@Count(array)항목 개수
집계@Sum(array)합계
집계@Avg(array)평균
집계@Min(array)최솟값
집계@Max(array)최댓값
선택@First(array)첫 번째 항목
선택@Last(array)마지막 항목
수학@Round(number, decimals?)소수 자릿수를 지정해 반올림. 기본 0자리
수학@Abs(number)절댓값
수학@Floor(number)내림
수학@Ceil(number)올림
반복@Each(array, "변수명", template)항목마다 템플릿을 평가해 UI 또는 값 배열 생성
  • @Filter 연산자: ==, !=, >, <, >=, <=, contains. contains는 대소문자를 구분하는 문자열 포함 검사다.
  • Filter와 Sort는 "user.name"처럼 중첩 필드 경로를 지원한다. Sort는 원본 배열을 복사해 정렬한다.
  • tickets.rows.amount처럼 객체 배열의 필드를 배열로 꺼내 집계할 수 있다. 함수 중첩도 가능하다.
  • @Each의 반복 변수는 템플릿 표현식 내부에서 사용한다. 템플릿을 별도 문장으로 분리하지 않는다.

아래는 전체 UI 프로그램이 아닌 데이터 가공 부분이다.

openTickets = @Filter(tickets.rows, "status", "==", "open")
sorted = @Sort(openTickets, "priority", "desc")
count = @Count(openTickets)
total = @Sum(openTickets.amount)
tags = @Each(sorted, "t", Tag(t.title))
openCount = @Count(@Filter(tickets.rows, "status", "==", "open"))

LLM은 함수 호출 코드를 생성하고, 클라이언트 렌더러가 실제 데이터로 계산한다. 데이터나 참조 상태가 바뀌면 관련 표현식을 다시 평가하므로, 필터·정렬·집계마다 LLM을 재호출할 필요가 없다.

내장 액션과 반응형 기능​

액션기능
@Run(ref)선언된 Mutation 실행 또는 Query 재조회
@Set($var, value)상태 변수 변경
@Reset($var1, $var2, …)상태 변수를 선언 당시 기본값으로 복원
@ToAssistant(message, context?)어시스턴트에 메시지와 선택적 컨텍스트 전달
@OpenUrl(url)URL 이동 이벤트. 실제 이동 처리는 호스트 UI와 연결

여러 액션은 Action([...])으로 묶어 순서대로 실행한다.

saveTicket = Mutation("save_ticket", {title: $title})
tickets = Query("list_tickets", {}, {rows: []})
saveBtn = Button("저장", Action([@Run(saveTicket), @Run(tickets), @Reset($title)]))

이 예시는 $title 선언과 도구 등록을 생략한 액션 연결 부분이다. 서버의 조회·변경 작업은 앱에서 제공한 도구가 수행한다.

함께 제공하는 기능문법과 동작
반응형 상태$search = ""처럼 선언하고 입력 컴포넌트의 value에 바인딩
데이터 조회Query(...)로 등록된 도구 호출. 인자에 사용한 상태가 바뀌면 재조회
데이터 변경Mutation(...)으로 작업을 선언하고 @Run으로 실행
조건부 렌더링조건 ? 컴포넌트 : null로 표시 여부 결정
사용자 정의 계산defineFunction()으로 함수를 추가하고 프롬프트·렌더러에 같은 라이브러리 연결
사용자 정의 동작defineAction()으로 액션을 추가하고 onAction에서 실제 동작 처리

확인한 내장 목록에는 @GroupBy, @Distinct, @Join, @Reduce, 날짜·통화 포맷 함수가 없다. 계산·포맷은 커스텀 함수로, 서버 데이터 처리나 비동기 작업은 도구/API로 확장한다.

GitHub 근거: 내장 함수·액션 구현, 함수와 확장 문서, 액션 문서, 반응형 상태 문서.

질문, openui-lang을 사용하게 되면 그럼 마크다운 출력대신에 openui lang으로만 대답하게 되는거야 ?​

OpenUI 모드만 사용해도 일반 텍스트와 Markdown을 충분히 출력할 수 있다- . 현재 프로젝트는 LLM 응답의 바깥 형식을 OpenUI Lang으로 생성하도록 지시하지만, 그 안의 TextContent와 MarkDownRenderer에 설명 문장이나 Markdown을 담아 렌더링한다.

  • 따라서 텍스트 설명을 제공하기 위해 반드시 inlineMode로 전환할 필요는 없다.

1. 현재 프로젝트 설정: 순수 UI 모드 (Pure Mode)​

현재 backend-langgraph/agent/system-prompt.txt의 첫 문장은 다음과 같이 선언되어 있다.

You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang.

  • 응답 형식과 표시 내용을 구분한다: “OpenUI Lang으로만 응답”은 LLM이 반환하는 외부 문법에 대한 지침이다. 화면의 모든 내용이 차트나 폼이어야 한다거나, 인사말·설명·Markdown을 표시할 수 없다는 뜻이 아니다. 인사와 설명도 OpenUI 컴포넌트 안에 넣으면 된다.

  • 일반 텍스트: TextContent에 서식 없는 문자열을 넣으면 화면에는 보통 채팅 문장처럼 보인다. 예를 들어 Hi에 대한 응답도 다음처럼 생성할 수 있다.

    root = Stack([greeting])
    greeting = TextContent("Hello! How can I assist you today?")

    사용자는 “Hello! How can I assist you today?”라는 문장을 보지만, 실제 전송된 응답은 OpenUI Lang이고 프런트엔드는 이를 React 컴포넌트로 렌더링한다. 별도의 plain-text 응답 모드로 바뀐 것이 아니다.

  • Markdown 텍스트: 현재 컴포넌트 명세에서 TextContent는 Markdown을 지원하고, MarkDownRenderer는 Markdown 텍스트 렌더링을 명시적으로 제공한다. 제목·강조·목록·링크 등의 문법을 컴포넌트 문자열에 담을 수 있다.

    root = Stack([intro, explanation])
    intro = TextContent("아래는 **프로젝트 요약**입니다.")
    explanation = MarkDownRenderer("## 주요 내용\n- **강조**와 *기울임*\n- 일반적인 설명 문장\n- [참고 링크](https://www.openui.com)")

    위의 \n은 문자열 내부의 줄바꿈이다. 응답 전체는 OpenUI Lang이지만 사용자는 제목·강조·목록·링크가 적용된 Markdown 문서를 보게 된다. Markdown 설명과 표·차트도 같은 Stack이나 Card 안에 함께 배치할 수 있다.

  • 표시 목적에 맞게 선택한다: 짧은 설명은 TextContent, 문서형 Markdown 블록은 MarkDownRenderer, 구조화된 데이터는 Table·차트 등으로 표현하면 된다. 일반적인 텍스트·Markdown 답변과 UI를 함께 보여주는 데 순수 UI 모드만으로 충분하다.

  • 엄격한 원문 텍스트는 별도 요구사항이다: TextContent도 Markdown을 해석하므로 **문자열**의 별표까지 그대로 보여주려는 경우는 서식 없는 문장을 표시하는 것과 다르다. 해당 목적에는 이스케이프, 코드 표시 또는 별도 plain-text 컴포넌트가 필요하다.

  • 현재 명세 근거: 프런트엔드 컴포넌트 계약 (frontend/src/generated/spec.json)의 TextContent(text, size?)와 MarkDownRenderer(textMarkdown, variant?)에 해당 지원이 정의되어 있다. 이 설명은 출력·렌더링 방식에 대한 문서 보완이며, 현재 프롬프트나 실행 모드를 변경한 것은 아니다.

2. OpenUI의 인라인 모드 (inlineMode: 텍스트 + UI 혼합)​

OpenUI 사양(@openuidev/lang-core)에는 inlineMode: true 옵션이 존재한다. 시스템 프롬프트 생성 시 promptOptions: { inlineMode: true }를 주면 프롬프트에 ## Inline Mode 지침이 포함된다.

  • 단순 질문일 때: UI 코드를 생성하지 않고 일반 챗봇처럼 순수 텍스트/마크다운으로만 응답한다 (기존 UI 유지).

  • UI 생성/수정 요청일 때: 마크다운 텍스트 설명과 함께 openui-lang ... 코드 펜스로 UI 코드를 감싸서 출력한다.

    요청하신 매출 통계 대시보드입니다:

    ```openui-lang
    root = Stack([title, chart])
    title = TextContent("Q4 매출 현황", "large-heavy")
    chart = BarChart(labels, [s1])
    labels = ["10월", "11월", "12월"]
    s1 = Series("매출", [100, 150, 200])
    ```

    추가로 수정이 필요한 부분이 있다면 알려주세요.

3. 프런트엔드의 파싱 및 렌더링 분기 원리​

프런트엔드 라이브러리(@openuidev/react-ui)의 readMessage와 hasLangSyntax 유틸리티는 응답 본문에서 ```openui-lang 코드 펜스나 root =` 선언을 감지한다.

  • ```openui-lang 펜스가 있으면: 펜스 외부의 텍스트는 일반 채팅 메시지(Markdown 버블)로 렌더링하고, 펜스 내부 코드는 OpenUI 런타임 렌더러(Renderer`)로 전달해 상호작용 가능한 React 컴포넌트로 렌더링한다.
  • 펜스 없이 전체가 순수 코드인 경우: 전체를 UI 캔버스로 바로 렌더링한다.

4. 두 모드 요약 비교​

구분순수 UI 모드 (현재 프로젝트 기본값)인라인 모드 (inlineMode: true)
LLM 전체 응답OpenUI Lang 코드 안의 컴포넌트 문자열로 인사·설명·Markdown을 표현일반 마크다운 대화 + ````openui-lang 코드 펜스
마크다운 서식 지원MarkDownRenderer, TextContent 컴포넌트 내부에서 지원일반 채팅 말풍선 본문에서 직접 지원
적합한 화면 구조Claude Artifacts 캔버스 전용 뷰, 단독 대시보드ChatGPT / v0 형태의 대화형 인터페이스

본진 근거: 인라인 모드 프롬프트 생성 로직 (../../22-reverse-engineering/openui/packages/lang-core/src/parser/prompt.ts#L386-L410), 시스템 프롬프트 명세 문서 (../../22-reverse-engineering/openui/docs/content/docs/openui-lang/system-prompts.mdx#L98), 메시지 파서 및 문법 감지 (../../22-reverse-engineering/openui/packages/react-ui/src/utils/messages.ts#L52).

질문: document-comparison의 “Behind the scenes / Used search documents”는 어떻게 표시되는가?​

질문: openui/examples/cookbooks/document-comparison에서 다음처럼 LLM이 작업하는 과정이 보였다. 이 화면은 어떻게 구현한 것인가?

Behind the scenes
Used search documents
Used search documents
Used search documents

답변: OpenUI의 AgentInterface가 도구 호출 이벤트를 받아 자동으로 표시하는 실행 타임라인이다. LLM이 Used search documents라는 문장을 직접 생성하는 것이 아니다. 아래 내용은 2026-10-18 현재 소스 확인 기준이다.

실행부터 표시까지의 흐름​

모델이 search_documents 호출 요청
↓
서버의 OpenAI SDK runTools()가 도구 실행
↓
도구 호출 정보가 응답 스트림에 포함
↓
openAIReadableStreamAdapter가 이벤트로 변환
↓
AgentInterface가 도구 활동을 관리
↓
ToolCallTimeline이 Behind the scenes 표시
  1. 서버의 도구 등록·실행: src/app/api/chat/route.ts에서 openai.chat.completions.runTools()에 search_documents와 실행 함수를 등록한다. 모델이 호출을 요청하면 SDK가 도구를 실행하고 결과를 모델에 전달한다. runner.toReadableStream()으로 스트림을 브라우저에 전달한다. 이 프로젝트의 해당 경로는 DeepAgents를 사용하지 않는다.
  2. 브라우저의 이벤트 변환: src/components/comparison-chat.tsx의 fetchLLM()에 openAIReadableStreamAdapter()가 연결되어 있다. 어댑터는 스트림의 delta.tool_calls를 읽어 TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END로 변환한다.
  3. 도구 활동과 답변 구분: 본진 AgentInterface/Thread.tsx가 도구 호출과 결과를 연결한 활동 목록을 만들고, ToolCallTimeline에 전달한다. 공개된 중간 설명이 있으면 타임라인의 텍스트 항목으로도 처리한다. 최종 답변은 별도 답변 영역에 표시한다.
  4. 표시 문구 생성: toolLabels.tsx가 search_documents를 search documents로 바꾼다. 실행 중에는 Using search documents, 완료 상태 표시는 Used search documents가 된다. ToolCallTimeline.tsx는 진행 중 제목을 Working, 작업 종료 후 제목을 Behind the scenes로 표시한다.
// document-comparison/src/components/comparison-chat.tsx
const llm = fetchLLM({
url: "/api/chat",
streamAdapter: openAIReadableStreamAdapter(),
messageFormat: openAIMessageFormat,
});

같은 문구가 세 번 나오는 이유​

UI가 같은 이름의 도구 호출을 세 건 표시한 것이다. 현재 src/lib/prompt.ts는 비교 기준마다 search_documents를 호출하고, 중요한 정보가 없으면 검색 표현을 바꾸어 재검색하도록 지시한다. 따라서 세 줄은 서로 다른 비교 기준의 검색이나 재검색일 수 있다. 정확한 검색 내용과 이유는 각 호출 인자를 확인해야 하며, 표시 문구만으로 세 문서를 각각 검색했다고 단정할 수 없다.

Inline 모드 및 내부 사고 과정과의 관계​

  • 이 타임라인은 도구 호출과 공개된 중간 설명을 표시하는 기능이며, 모델의 비공개 내부 사고 과정을 그대로 보여주는 기능이 아니다.
  • Used search documents는 UI가 만든 상태 문구이므로 일반 텍스트로 보이더라도 Inline 모드라는 증거가 아니다. 순수 OpenUI Lang 출력을 지시하는 현재 설정에서도 나타난다.
  • 최종 OpenUI 응답의 렌더링과 도구 활동 타임라인은 별도 경로다. 앞서 나눈 학습 레이어에서는 3번 에이전트 실행 → 4번 스트림 전달 → 6번 채팅 화면·대화 상태에 걸친 기능이다.
  • 어댑터의 TOOL_CALL_END는 모델 스트림의 finish_reason: "tool_calls"를 보고 생성된다. 이 이벤트 자체를 서버 도구 실행의 성공 증거와 동일하게 해석해서는 안 된다. 실제 결과·오류 검증에는 도구 결과 메시지까지 확인해야 한다.

코드 근거:

  • 서버 도구 실행 및 스트림 전달 (../../22-reverse-engineering/openui/examples/cookbooks/document-comparison/src/app/api/chat/route.ts)
  • 프런트엔드 어댑터 연결 (../../22-reverse-engineering/openui/examples/cookbooks/document-comparison/src/components/comparison-chat.tsx)
  • 검색 횟수와 재검색 지침 (../../22-reverse-engineering/openui/examples/cookbooks/document-comparison/src/lib/prompt.ts)
  • OpenAI 스트림을 도구 이벤트로 변환 (../../22-reverse-engineering/openui/packages/react-headless/src/stream/adapters/openai-readable-stream.ts)
  • 도구 활동 및 답변 영역 구성 (../../22-reverse-engineering/openui/packages/react-ui/src/components/AgentInterface/Thread.tsx)
  • 도구 이름과 상태 문구 생성 (../../22-reverse-engineering/openui/packages/react-ui/src/components/ToolCall/toolLabels.tsx)
  • Behind the scenes 타임라인 (../../22-reverse-engineering/openui/packages/react-ui/src/components/ToolCall/ToolCallTimeline.tsx)

Prompt Library 확장​

ui를 만드는 과정에서 openUI에서 ui가 완성중이면 인터렉션 ui(폼, 카드)등 disable 시키는 기능이 있니?​

답변: 있다. OpenUI 기본 컴포넌트에는 스트리밍 중 입력·클릭을 비활성화하는 처리가 구현되어 있다. 핵심은 @openuidev/react-lang의 useIsStreaming() 훅이다.

// OpenUI Renderer 내부에서 렌더링되는 커스텀 컴포넌트 예시
const isStreaming = useIsStreaming();

return <button disabled={isStreaming}>제출</button>;
대상스트리밍 중 동작
Button, IconButtondisabled={isStreaming}으로 버튼 비활성화
Input, TextArea, Select, Slider입력·변경 비활성화
CheckBoxGroup, RadioGroup, OptionCards 등선택 비활성화
OverviewCardBlock, ContextCardBlock 등 액션 카드클릭 가능 상태 해제 및 액션 실행 차단

예를 들어 액션 카드는 Boolean(props.action) && !isStreaming으로 클릭 가능 여부를 정하고, 클릭 처리 함수에서도 isStreaming이면 즉시 반환한다.

적용 범위와 주의점​

  • 화면 전체를 한꺼번에 잠그는 방식은 아니다. Form 컨테이너가 전체를 잠그는 것이 아니라, 내부 입력·버튼 컴포넌트가 각각 스트리밍 상태를 읽어 비활성화한다. 모든 컴포넌트의 모든 상호작용이 자동 차단된다고 일반화하면 안 된다.
  • 커스텀 컴포넌트에는 직접 적용한다. useIsStreaming()을 읽어 disabled에 연결하거나 클릭·액션 처리에서 실행을 차단한다.
  • 기준은 해당 렌더러에 전달된 isStreaming이다. 카드 하나의 코드가 먼저 완성됐는지를 판정하는 기능이 아니다. 직접 <Renderer />를 사용하는 화면은 호스트가 실제 응답 생성 상태에 맞춰 이 값을 전달해야 한다.
  • 스트리밍 종료와 UI 유효성 검증은 별개다. isStreaming이 false가 되었다고 생성된 UI가 문법·스키마 검증을 통과했다는 뜻은 아니다.

2026-10-18 본진 소스 확인 기준이다. 이번 확인에서는 브라우저에서 생성 중 입력·클릭을 직접 시험하지 않았으며, 런타임 코드는 변경하지 않았다.

코드 근거:

  • useIsStreaming 정의 (../../22-reverse-engineering/openui/packages/react-lang/src/context.ts)
  • Button 비활성화 (../../22-reverse-engineering/openui/packages/react-ui/src/genui-lib/Button/index.tsx)
  • Input 비활성화 (../../22-reverse-engineering/openui/packages/react-ui/src/genui-lib/Input/index.tsx)
  • OverviewCardBlock 클릭 차단 (../../22-reverse-engineering/openui/packages/react-ui/src/genui-lib/OverviewCardBlock/index.tsx)
  • ContextCardBlock 클릭 차단 (../../22-reverse-engineering/openui/packages/react-ui/src/genui-lib/ContextCardBlock/index.tsx)
  • Form 컨테이너 구현 (../../22-reverse-engineering/openui/packages/react-ui/src/genui-lib/Form/index.tsx)