본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 26. 21:28

Generative UI에 JSON은 최적인가? OpenUI라는 선택지

요약

Generative UI의 다양한 구현 방식과 출력 형식(JSON, HTML, DSL)을 비교 분석합니다. 특히 Thesys가 개발한 OpenUI 프레임워크가 독자적인 언어인 OpenUI Lang을 통해 토큰 효율성과 출력 견고성을 어떻게 확보하는지 다룹니다.

핵심 포인트

  • Generative UI는 텍스트를 넘어 조작 가능한 인터랙티브 UI를 동적으로 생성하는 기술임
  • 구현 방식은 자유도가 높은 프리폼 생성형과 안전한 컴포넌트 주도형으로 나뉨
  • OpenUI는 토큰 효율과 스트리밍을 위해 독자적인 DSL(OpenUI Lang)을 채택함
  • 핵심 과제는 AI에게 UI를 만드는 것이 아니라 의도대로 출력하게 만드는 설계에 있음

서론

최근, Generative UI를 둘러싼 선택지가 단숨에 늘어났습니다.

A2UI

json-render

MCP Apps

Open Generative UI

, 그리고 이번에 다룰 OpenUI.

프로토콜, 라이브러리, 프레임워크가 뒤섞여 있어, 한데 묶어 '프레임워크'라고 부르기 어려울 정도로 유행하고 있습니다.

그중에서도 OpenUI는 Thesys가 개발하는 풀스택 Generative UI 프레임워크입니다. UI 출력에 OpenUI Lang이라는 독자적인 언어를 사용하는 것이 가장 큰 특징이며, **토큰 효율(Token Efficiency) · 스트리밍(Streaming) · 출력의 견고성(Robustness)**을 목표로 설계되었습니다.

Generative UI라는 용어 자체는 2023년 10월 Vercel의 v0로부터 널리 알려지게 되었습니다. 그로부터 2024년 3월에는 Vercel AI SDK 3.0에서 Generative UI 대응(v0의 생성 UI 기술의 OSS화)이 이루어졌고, Claude의 Artifacts, ChatGPT의 Canvas로 이어지며 2025년 이후에는 생태계가 단숨에 확장되었습니다. 채팅 UI(텍스트의 벽) → Artifacts / Canvas(생성물을 옆에 나란히 배치) → Generative UI(조작 가능한 UI를 생성)라는 흐름으로 이해하면 쉬울 것입니다.

그리고 비슷한 속도로, AI가 '어떤 형식으로 UI를 출력할 것인가'에 대한 선택지도 늘어나고 있습니다.

예를 들어, A2UIjson-renderJSON을, MCP Apps나 CopilotKit의 Open Generative UIHTML을, 그리고 이번 주인공인 OpenUI는 **OpenUI Lang이라는 독자적인 언어(DSL)**를 선택하고 있습니다.

그래서 이번에는 OpenUI를 사용하여 태스크 관리 앱을 실제로 만들어 보았기에, Generative UI에서 무엇이 변하는지를 다시 한번 정리한 후, 출력 형식의 선택지(JSON / HTML / DSL)를 비교하며 OpenUI가 왜 독자적인 언어를 선택했는지, 즉 OpenUI의 핵심에 대해 해설해 보고자 합니다.

또한, 실제로 만들어 보면서 무엇이 어려웠는지도 함께 적어보도록 하겠습니다.

Generative UI란

Generative UI란, LLM이 사용자의 프롬프트나 문맥에 따라 텍스트뿐만 아니라 인터랙티브한(Interactive) UI 그 자체를 동적으로 생성·그리는 모달리티(Modality)를 말합니다.

예를 들어 "오늘 날씨는?"이라고 물으면 날씨 카드가, "매출을 비교해 줘"라고 하면 막대그래프가 그 자리에서 생성되어 돌아옵니다. 응답이 '텍스트'가 아니라 '조작 가능한 경험'이 된다는 것이 핵심 컨셉입니다.

구현 방식은 크게 두 가지로 나뉩니다.

프리폼 생성형(Free-form Generation): AI에게 HTML/CSS 등을 자유롭게 쓰게 한다. 표현력은 높지만, 무엇이 나올지 보장하기 어렵다 -
컴포넌트 주도형(Component-driven): 미리 준비한 컴포넌트의 조합만을 생성하게 한다. 안전하지만, 낼 수 있는 UI는 라이브러리의 범위 내로 한정된다

이 기사에서 다루는 JSON / DSL 계열(A2UI · json-render · OpenUI)은 컴포넌트 주도형에 해당합니다. 반면, HTML 계열 중 Open Generative UI는 프리폼 생성형에 가깝고, MCP Apps는 서버가 준비한 HTML을 전달하는 중간적인 위치에 있습니다.

이 중 어느 쪽을 선택하느냐가 이후의 '출력 형식' 이야기로 이어집니다.

그리고 실제로 OpenUI로 만들어 보면서 가장 강하게 느낀 점은, 어려운 것은 "AI에게 UI를 만들게 하는 것"이 아니라 "AI에게 의도대로의 UI를 출력하게 하는 것"이었습니다.

이는 출력 형식의 설계에 관한 이야기이기도 합니다.

UI는 "만드는 것"에서 "생성되는 것"으로

지금까지 프론트엔드의 업무는 인간이 볼 화면을 사전에 다 만들어 두는 것이었습니다.

디자인을 결정하고, 컴포넌트를 구현하고, 상태에 따라 나누어 보여준다. UI는 출시 전에 완성되어 있는 정적인 결과물이라는 위치였습니다.

하지만 Generative UI는 이 전제를 바꿉니다.

Generative UI에 있어서 UI는 AI가 응답할 때마다 그 자리에서 생성하는 동적인 출력물이 됩니다.

이것은 프론트엔드(Frontend)의 상대방에 AI가 가세하기 시작하고 있다는 흐름의 일부를 반영하는 현상이라고 볼 수 있습니다.

실제로 AI는 웹(Web) 정보를 읽어들이고(소비), 웹 앱을 조작하며(소비), 그리고 UI 그 자체를 생성하는(생산자) 존재가 되었습니다.

즉, AI는 프론트엔드에게 있어 새로운 '만드는 이'가 되어가고 있습니다. 그렇게 되면 중요해지는 것은, 그 만드는 이에게 무엇을, 어떤 형식으로 만들게 할 것인가라는 설계 부분입니다.

어려운 것은 UI 생성이 아니라 "출력 계약" 설계

실제로 직접 해보니, 어려움이 느껴지는 지점이 예상과는 다르다는 것을 깨달았습니다.

LLM에게 "UI를 만들어줘"라고 요청하는 것 자체는 이제 어렵지 않습니다. 어려운 것은 그 출력을 애플리케이션 측에서 안전하고 확실하게 렌더링(Rendering)할 수 있는 형태로 유지하는 것입니다.

구체적으로는 다음 4가지를 설계해야 합니다.

출력 가능한 UI의 범위: AI가 마음대로 컴포넌트(Component)를 만들어내면 시스템이 붕괴된다. 허용된 부품으로만 한정한다 -
스트리밍 (Streaming): 응답은 조금씩 전달된다. 중간의 불완전한 출력에서도 깨지지 않는 묘사 -
검증 (Validation): LLM의 출력은 신뢰할 수 없는 외부 입력이다. 타입(Type)이나 구조의 검증 -
컴포넌트로의 안전한 매핑 (Mapping): 출력을 실제로 존재하는 React 컴포넌트로 안전하게 대응시킨다

OpenUI의 문서에서는 컴포넌트 라이브러리(Component library)를 앱과 AI 사이의 **계약 (Contract)**이라고 설명합니다. json-render에서는 **카탈로그 (Catalog)**를 가드레일(Guardrail)이라고 부르는 등 용어는 프레임워크마다 다르지만, 모두 AI가 무엇을, 어떤 형식으로 출력해도 되는가라는 약속의 설계가 핵심입니다. 이 글에서는 이것들을 통칭하여 "출력 계약"이라고 부르기로 하겠습니다.

계약의 내용이 결정되더라도, LLM이 그것을 어떻게 작성할지는 별개의 문제입니다. BarChart를 사용한다는 합의가 있더라도, 그것을 **JSON 트리 (JSON Tree)**로 내뱉게 할 것인지, DSL의 한 줄씩으로 할 것인지, 아니면 HTML로 할 것인지와 같은 출력 형식의 선택이 첫 번째 큰 분기점이 됩니다.

누구나 가장 먼저 손에 쥐는 것은 JSON입니다. JSON은 데이터 표현의 표준이기 때문입니다.

하지만, Generative UI에 있어서 JSON은 정말로 최적일까요?

기존의 Generative UI 수법 정리

OpenUI를 보기 전에 주변의 수법들을 정리해 두겠습니다. Generative UI의 출력 형식은 현재 크게 JSON 트리 / HTML / DSL의 삼파전 양상을 띠고 있습니다.

CopilotKit은 Generative UI를 미리 준비된 컴포넌트를 삽입하는 Static, HTML/CSS/JS까지 자유롭게 생성하는 Open-Ended, 구조화된 사양으로부터 UI를 그리는 Declarative의 3가지로 정리하고 있습니다. [1]

또한, AG-UI와 A2UI의 관계에 대해서는, AG-UI는 에이전트(Agent)와 앱 사이의 통신 프로토콜이며, A2UI는 UI를 선언적으로 나타내는 사양으로, 양자는 경쟁 관계가 아닌 보완 관계에 있다고 설명합니다. [2]

이 글에서는 그 정리를 조금 확장하여, 통신의 계층UI 표현의 계층을 나누어 생각하겠습니다. AG-UI는 "어떤 버튼이나 카드를 그릴 것인가"를 결정하는 사양이 아니라, 에이전트와 프론트엔드 사이에서 메시지, 상태 업데이트, 사용자 조작, UI 생성 결과를 흘려보내기 위한 프로토콜입니다. [3]

그 바탕 위에서 실제로 UI를 어떻게 표현할지는 JSON, HTML, DSL 등의 각 방식이 담당합니다.

그런 관점에서 보면 주요 수법은 다음과 같이 정리할 수 있습니다.

수법주체출력 형식카테고리스트리밍
A2UIJSON 컴포넌트 트리JSON
...HTML✕ (UI는 정적)
Open Generative UICopilotKit생(Raw) HTML/CSS/JS를 sandbox iframe으로HTML
OpenUIThesysOpenUI Lang (DSL)DSL

그럼 세 가지 주류를 차례대로 살펴보겠습니다.

JSON으로 UI를 표현하기

이 절에서는 몇 가지 전문 용어가 등장하므로, 먼저 용어집을 기재해 두겠습니다.

용어이 글에서의 의미구체적인 예시
컴포넌트 트리 (Component Tree)UI 부품의 부모-자식 관계. "Card 안에 Text와 Button이 있다"와 같은 구조Card > Text + Button
노드 (Node) / 엘리먼트 (Element)트리를 구성하는 하나하나의 UI 부품card-1, text-1, button-1
플랫한 리스트 (Flat List) / 맵 (Map)부모-자식을 중첩해서 쓰지 않고, 각 부품을 id와 함께 나열하여 참조로 연결하는 방식children: ["text-1", "button-1"]
카탈로그 (Catalog)AI가 사용할 수 있는 컴포넌트 이름과 props 정의 목록Card.title, Text.content, Button.label
JSON Patch"여기에 요소를 추가한다", "이 값을 교체한다"와 같이 JSON에 대한 차분(diff) 조작{"op":"add","path":"/components/card-1"}
JSONL한 줄에 하나의 JSON을 배치하는 형식. 스트리밍(Streaming)으로 조금씩 보낼 때 사용하기 용이함1행에 Card, 2행에 Text를 추가

먼저 정석적인 방법인, UI를 데이터 (JSON)로 표현하는 방식입니다.

여기서 말하는 JSON은 완성된 HTML이 아닙니다. AI는 "화면에 Card를 배치한다", "그 안에 Text를 배치한다"와 같은 부품 명세서를 JSON으로 반환하고, 앱 측에서 이를 실제 React 컴포넌트 등에 대응시켜 렌더링합니다.

이미지로 표현하자면 다음과 같은 JSON입니다.

{
"root": "card-1",
"components": {
...

이것은 HTML도 React 코드도 아니며, 어디까지나 "어떤 부품을, 어떤 props로, 어떤 순서로 배치할 것인가"를 나타내는 데이터입니다. 앱 측은 CardTextButton이라는 이름을 자신이 보유한 안전한 컴포넌트에 대응시켜 렌더링합니다.

"코드가 아닌 데이터를 보낸다"를 통해 실행 가능한 범위를 제한하고 안전성을 확보하는 것이 공통된 발상입니다.

즉, JSON 방식에서는 AI가 "화면 그 자체"가 아니라, 화면을 조립하기 위한 설계도를 반환합니다.

A2UI (Agent-to-User Interface)

A2UI는 Google이 2025년 12월에 공개한 Apache-2.0 라이선스의 오픈 프로토콜입니다. "JSON으로 UI를 표현하는" 방식의 대표적인 사례로, 에이전트가 화면 조립 방법을 JSON으로 보내면 앱이 이를 읽어 렌더링합니다. 앞서 언급한 AG-UI와 연동할 수 있으며, 에이전트 간 통신 규격인 A2A를 위한 UI 기술로도 사용됩니다.

A2UI의 JSON은 화면을 구성하는 부품을 components라는 배열에 나열합니다. 핵심은 부품을 깊게 중첩하지 않고, 우선 나란히 배치하는 것입니다. 부품끼리는 id로 서로 참조하며, 표시할 텍스트나 수치는 별도의 데이터 모델에서 {"path": ...}로 가져옵니다.

예를 들어 다음 예시에서는 root-column이라는 Column이 있고, 그 자식으로 title을 참조하고 있습니다. title의 실체는 동일한 components 배열 안에 별도의 노드로 정의되어 있습니다.

{
"surfaceUpdate": {
"surfaceId": "default",
...

앱 측은 미리 Button이나 Text 등 사용할 수 있는 UI 부품을 정해둡니다. 전달받은 JSON은 해당 부품들의 조합으로 해석하며, React, Angular, Lit, Flutter, Markdown용 공식 구현체를 통해 화면으로 구현할 수 있습니다.

플랫하게 나열하는 장점은 부분적으로 전달된 JSON이라도 다루기 쉽다는 점입니다.

깊게 중첩된 JSON은 마지막 }가 도착할 때까지 전체 구조가 확정되지 않습니다. 반면 A2UI처럼 부품을 id로 분리해 두면, LLM이 조금씩 부품을 추가하며 화면을 조립하는 스트리밍(Streaming) 방식과 궁합이 좋습니다.

json-render

json-render는 Vercel이 2026년 1월에 공개한 Generative UI 프레임워크입니다.

json-render 또한 A2UI와 마찬가지로 UI를 JSON 설계도로 표현합니다.

다만, 배열이 아닌 **element 맵 (element map)**을 사용합니다. element 맵을 간단히 설명하자면, "id를 키(key)로 하는 UI 부품의 사전"입니다.

element 맵에서는 아래 예시와 같이 root가 가장 먼저 그릴 부품을 가리키고, elements 안에 실제 부품 정의가 나열되는 구성을 취합니다.

{
"root": "card-1",
"elements": {
...

허용하는 컴포넌트(component)나 props는 Zod로 작성한 **카탈로그 (catalog)**로 제약합니다.

여기서 말하는 카탈로그는 AI가 사용할 수 있는 부품 목록입니다. 위의 JSON이 "AI의 출력"이라면, 카탈로그는 그 출력을 제한하기 위해 앱 측에서 미리 준비해 두는 정의입니다.

예를 들어, 개념적으로는 다음과 같습니다.

const catalog = {
Card: z.object({
title: z.string(),
...

이 카탈로그가 있음으로써 AI는 CardText를 사용할 수 있지만, 존재하지 않는 FancyChart를 마음대로 출력하거나 Card에 정의되지 않은 props를 전달하면 검증(validation) 단계에서 차단됩니다.

이른바 유효성 검사(validation)와 같은 개념으로, json-render에서는 이를 카탈로그라고 부릅니다.

스트리밍(streaming)은 SpecStream이라는 JSONL로 수행합니다. 여기서 흘러나오는 것은 완성된 형태의 거대한 JSON이 아니라, 한 줄 단위의 JSON Patch 작업입니다. 즉, "/elements/card-1에 Card를 추가한다"와 같은 차분(diff)이 순차적으로 도착하며, 패치(patch)가 도착할 때마다 UI가 조립되어 갑니다.

{"op":"add","path":"/elements/card-1","value":{"type":"Card", ...}}

HTML을 생성하기

다음은 HTML 그 자체를 출력 및 전달하는 방식입니다.

MCP Apps

MCP Apps는 MCP 서버가 채팅이나 에이전트 앱 안에 HTML 기반 UI를 삽입할 수 있도록 하는 공식 확장입니다. 모태가 된 제안은 SEP-1865로, 2026년 1월에 병합되었으며, 현재는 modelcontextprotocol/ext-apps를 통해 Stable 버전 사양으로 공개되어 있습니다. 이는 커뮤니티의 mcp-ui 프로젝트를 정식으로 수용한 것입니다.

A2UI나 json-render가 "UI의 설계도"를 JSON으로 반환하는 것에 반해, MCP Apps는 HTML 그 자체를 반환합니다.

MCP 서버는 HTML을 ui://로 시작하는 **UI 리소스 (UI resource)**로 공개하며, 호스트 앱(MCP 서버를 호출하는 측의 앱)은 MCP의 resources/read를 통해 해당 HTML을 읽어옵니다. 읽어온 UI는 호스트 앱의 화면에 직접 섞여 들어가는 것이 아니라, 샌드박스화된 iframe (sandboxed iframe), 즉 외부 앱으로부터 격리된 작은 표시창 안에서 표시됩니다.

이 부분의 용어들은 조금 이해하기 어려울 수 있어 아래 표로 정리해 두었습니다.

용어의미예시
호스트 앱 (Host App)MCP 서버를 호출하고, 반환된 UI를 표시하는 측의 앱Claude Desktop, IDE, 채팅 앱
ui:// UI 리소스MCP 서버가 공개하는 HTML UI의 위치ui://weather/detail.html
샌드박스화된 iframe수신한 HTML을 호스트 앱 본체와는 별개의 "격리 환경"에서 표시하는 메커니즘날씨 UI를 표시하더라도, 해당 HTML이 채팅 화면 본체를 마음대로 수정할 수 없음
JSON-RPCUI와 호스트 측이 함수 호출처럼 데이터를 주고받기 위한 JSON 기반 통신 형식iframe에서 "이 버튼이 눌렸다"라고 호스트에 전송함

데이터 교환은 이 iframe 내의 UI와 호스트 측 사이에서 JSON-RPC를 사용하여 수행합니다. 반면, UI 자체는 미리 준비된 HTML 템플릿으로서 로드되기 때문에, LLM이 토큰 단위로 UI를 조금씩 생성해 나가는 타입의 스트리밍(Streaming) 방식은 아닙니다.

Open Generative UI

CopilotKit의 Open Generative UI는 CopilotKit이 제공하는 Generative UI 기능 중 하나입니다. 공식 문서에서는 generateSandboxedUi를 사용하여, 에이전트가 그 자리에서 HTML/CSS/JS를 생성하고 이를 격리된 iframe에 표시하는 방식으로 소개하고 있습니다.

MCP Apps가 "서버 측에서 준비한 HTML을 UI 리소스로 배포하는" 방식이라면, Open Generative UI는 "에이전트가 응답 중에 HTML/CSS/JS를 작성하고, 그 결과를 샌드박스(Sandbox) 내에 표시하는" 방식입니다.

JSON 트리도 아니고, 사전에 정의된 React 컴포넌트를 삽입하는 방식도 아닌, "무엇이든 쓸 수 있는" 오픈 엔드(Open-ended) 형태인 것입니다.

자유도가 최대인 대신, 계약(Contract)에 의한 제어는 최소가 됩니다. 즉, json-render의 카탈로그처럼 "사용해도 좋은 부품 이름과 props"를 엄격하게 제한하기보다, LLM이 그 자리에서 적절한 HTML/CSS/JS를 작성할 수 있는지에 의존하는 부분이 크다는 의미입니다.

OpenUI — UI를 "언어"로서 생성하기

지금까지 "JSON"과 "HTML"이라는 두 가지 방식을 살펴보았습니다.

OpenUI는 그 둘 중 어느 것도 아닌 제3의 선택지를 채택하고 있습니다. 그것은 바로 UI를 기술하기 위한 언어 = OpenUI Lang을 LLM이 생성하게 만드는 방법입니다.

OpenUI의 처리 파이프라인

OpenUI는 이전 공정의 출력을 다음 공정이 받는 흐름으로 UI를 조립합니다. 공장의 컨베이어 벨트처럼 데이터가 일방통행으로 진행되므로, 이러한 연쇄를 **파이프라인 (Pipeline, 처리 흐름)**이라고 부릅니다.

Component Library → System Prompt → LLM → OpenUI Lang Stream → Parser → Renderer → Live UI

위의 그림을 설명하면 다음과 같은 흐름이 됩니다.

  • 개발자가 Component Library (사용 가능한 부품 목록)를 정의한다. - 그 정의로부터 System Prompt (AI를 위한 지시문)가 자동 생성된다. - LLM이 OpenUI Lang이라는 텍스트를 조금씩 반환한다 (Stream). - Parser가 텍스트를 해석하여 부품 조립도로 변환한다. - Renderer가 조립도를 실제 React 컴포넌트에 대응시켜 화면에 그린다.

이 중 OpenUI는 4가지 부품을 제공합니다.

  • Library: Zod 스키마 + React 렌더러로 컴포넌트를 정의한다 (defineComponent). AI가 출력해도 되는 부품의 계약 (Contract) 그 자체.
  • Prompt Generator: 라이브러리로부터 시스템 프롬프트를 자동 생성한다.
  • Parser: OpenUI Lang을 행 단위로 파싱한다 (스트리밍 대응).
  • Renderer: 파싱 결과를 React 컴포넌트에 대응시키고, 스트림의 도착에 맞춰 렌더링한다.

이 앱에서는 어떻게 연결했는가

이번 앱에서는 작업 목록을 표시하기 위한 TaskList 컴포넌트를 OpenUI의 Component Library에 추가했습니다. TaskList의 자세한 정의는 이후 컴포넌트 라이브러리가 계약이 되므로 나중에 설명하겠습니다. 여기서는 파이프라인 전체의 연결 방식만 파악해 두시면 됩니다.

서버 측에서는 OpenUI의 컴포넌트 사양에 TaskListlist_tasks 도구의 사양을 추가하여, LLM에게 전달할 systemPrompt를 생성하고 있습니다. 여기서 "읽기 계열은 TaskList(tasks)를 root로 한다", "list_tasks는 OpenUI Lang의 Query

와 같은 규칙(readListExample)도 함께 전달합니다.

const readListExample = `root = TaskList(tasks)
tasks = Query("list_tasks", {status: "all", sortBy: "createdAt", sortDirection: "asc"}, [])`;
const taskListSpec = {
...

클라이언트 측에서는 LLM으로부터 반환된 텍스트가 OpenUI Lang이라면, <Renderer>responsegenuiLibrary를 전달합니다. OpenUI 내부의 Parser가 root = ...와 같은 행을 해석하고, Renderer가 TaskList(...)를 실제 React 컴포넌트에 대응시켜 렌더링합니다.

{isLang ? (
<Renderer
isStreaming={isStreaming && isLastMessage}
...

이러한 연결을 통해, LLM이 반환한 root = TaskList(tasks)라는 OpenUI Lang이 최종적으로는 검색·필터·정렬 기능을 갖춘 React의 TaskList UI로 표시됩니다.

demo

즉, LLM 자체는 OpenUI의 부품이 아니지만, 파이프라인의 중심에서 "OpenUI Lang이라는 텍스트를 생성하는" 역할을 담당합니다. 그 텍스트를 Parser와 Renderer가 앱 측의 안전한 컴포넌트로 변환합니다.

OpenUI Lang 읽는 법

OpenUI Lang은 identifier = Expression 형태의 1행 1문으로 작성하는 선언적 DSL (Domain Specific Language)입니다. 공식 사양에 따르면, 첫 번째 문장은 반드시 root에 대한 할당으로 시작하며, 각 행이 UI의 부품·상태·데이터 취득 등을 정의하는 형식을 취하고 있습니다.

(첫 번째 행은 root = ...로 시작해야 합니다. root는 화면 전체의 진입점으로, "가장 먼저 어떤 UI 부품을 그릴 것인가"를 나타내는 이름입니다.)[4]

identifier = Expression이라고 쓰면 조금 어렵게 보일 수 있지만, 의미는 단순합니다. 좌변의 identifier는 "나중에 참조하기 위한 이름"이고, 우변의 Expression은 "만들고자 하는 것"입니다.

이름 = 만들고자 하는 것

예를 들어 title = TextContent("Tasks", "large-heavy")라면, "title이라는 이름으로 Tasks라는 헤드라인 텍스트를 만든다"는 의미가 됩니다. JSON처럼 거대한 객체를 중첩시키는 것이 아니라, UI 부품을 한 줄씩 이름을 붙여 나가는 이미지입니다.

우선 최소 예제로 살펴보면 다음과 같은 형태가 됩니다.

root = Stack([title, addButton])
title = TextContent("Tasks", "large-heavy")
addButton = Button("Add task")

이 3줄은 "먼저 root로서 Stack을 그리고, 그 안에 titleaddButton을 배치한다"는 의미입니다. Stack은 가로세로로 부품을 나열하는 레이아웃 부품이며, TextContent는 텍스트, Button은 버튼입니다.

흥미로운 점은 root의 우변에서 titleaddButton을 먼저 참조한 뒤, 그 다음 행에서 실체를 정의하고 있다는 점입니다. 보통이라면 "아직 정의되지 않은 title을 먼저 사용해도 괜찮은가?"라고 생각하겠지만, OpenUI Lang에서는 이것이 허용됩니다. 이처럼 OpenUI Lang에서는 전방 참조 (Forward Reference) (나중에 정의할 이름을 먼저 사용하는 것)가 가능합니다.

Renderer는 아직 정의가 도달하지 않은 부품을 일단 스켈레톤 (Skeleton) 형태로 표시하고, 후속 행이 도착하면 실제 UI로 교체합니다.

이 앱에서 실제로 모델에게 생성시키고 있는 OpenUI Lang은 훨씬 더 단순합니다. 태스크 목록을 표시할 때는 TaskList를 root로 삼고, list_tasksQuery 결과를 그대로 전달합니다.

root = TaskList(tasks)
tasks = Query("list_tasks", {status: "all", sortBy: "createdAt", sortDirection: "asc"}, [])

여기서 root = TaskList(tasks)는 "화면의 입구로서 TaskList를 그린다"는 의미입니다. 다음 줄의 tasks = Query(...)list_tasks 툴(tool)로 취득한 태스크 배열에 tasks라는 이름을 붙이고 있습니다.

최종적으로 RendererTaskList(tasks)를 이 앱에서 정의한 React의 TaskList 컴포넌트(Component)로 표시합니다.

그렇다면 이 DSL은 인간이 매번 수동으로 작성할까요? 기본적으로는 그렇지 않습니다. OpenUI Lang은 개발자가 정의한 컴포넌트 라이브러리(Component Library)와 시스템 프롬프트(System Prompt)를 바탕으로 LLM이 응답으로서 생성합니다.

이 앱에서도 서버 측에서 systemPrompt를 만들고, 그것을 streamText에 전달하여 모델이 OpenUI Lang을 출력하도록 하고 있습니다. systemPrompt에는 사용 가능한 컴포넌트, 사용 가능한 읽기(read) 툴, 그리고 출력 예시를 포함하고 있습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0