Open UI (React) + Langgraph
구축 레이어
- 컴포넌트 & 상태/액션 규칙
- 열 지향 테이블:
Table은 행(Row) 단위가 아닌Col단위의 열 중심 배열 구조 사용 (차트와의 데이터 연동 용이). - 폼(Form) 규칙: Form 중첩 금지, 스트리밍 지연 방지를 위해 각
FormControl을 개별 변수로 분리, 명시적Buttons필수. - 반응형 상태 & 액션:
$variable양방향 바인딩, 컬렉션 연산(@Filter,@Sort,@Count), 이벤트 트리거(@ToAssistant,@OpenUrl) 지원. - 카드 전용 인라인 요소 제약:
Text,BoldText,IconText,MetricIndicatorInline등은 카드 블록 내부에서만 사용 가능 (루트 직접 배치 불가).
UI 컴포넌트 카탈로그와 LangGraph 시스템 프롬프트 동기화 방법
LLM이 생성할 OpenUI 컴포넌트를 추가하거나 이름·인자·사용 규칙을 변경하면, 프런트엔드 렌더링 카탈로그와 백엔드 시스템 프롬프트를 함께 갱신해야 한다.
현재 방식: 생성 후 명시적으로 복사
- 새 컴포넌트 구현과 명세를 OpenUI 라이브러리에 등록하고, 실제 렌더러와
frontend/src/library.ts의 생성용 export가 같은 카탈로그를 사용하도록 맞춘다.
- 새 컴포넌트 구현과 명세를 OpenUI 라이브러리에 등록하고, 실제 렌더러와
frontend에서 프롬프트를 생성하고 백엔드 자산으로 복사한다.
- 변경된 프롬프트에 새 컴포넌트의 이름·인자·규칙이 포함됐는지 확인한다.
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/langchain | openUIStreamTransformer로 LangGraph 이벤트를 OpenUI 스트림으로 변환 | OpenUIStreamTransformer로 직접 구현 |
@openuidev/lang-core | generateSystemPrompt()로 컴포넌트 명세를 시스템 프롬프트로 변환 | 컴포넌트 카탈로그 SSOT는 FE에 있고, FE에서 Autogen으로 생성한 system-prompt.txt를 읽어 사용 |
deepagents | createDeepAgent()로 모델·도구·프롬프트를 결합한 에이전트 그래프 생성 | 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 표시
- 서버의 도구 등록·실행:
src/app/api/chat/route.ts에서openai.chat.completions.runTools()에search_documents와 실행 함수를 등록한다. 모델이 호출을 요청하면 SDK가 도구를 실행하고 결과를 모델에 전달한다.runner.toReadableStream()으로 스트림을 브라우저에 전달한다. 이 프로젝트의 해당 경로는 DeepAgents를 사용하지 않는다. - 브라우저의 이벤트 변환:
src/components/comparison-chat.tsx의fetchLLM()에openAIReadableStreamAdapter()가 연결되어 있다. 어댑터는 스트림의delta.tool_calls를 읽어TOOL_CALL_START,TOOL_CALL_ARGS,TOOL_CALL_END로 변환한다. - 도구 활동과 답변 구분: 본진
AgentInterface/Thread.tsx가 도구 호출과 결과를 연결한 활동 목록을 만들고,ToolCallTimeline에 전달한다. 공개된 중간 설명이 있으면 타임라인의 텍스트 항목으로도 처리한다. 최종 답변은 별도 답변 영역에 표시한다. - 표시 문구 생성:
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, IconButton | disabled={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)