@openuidev/*)는 LLM(거대 언어 모델)이 채팅 답변 대신 실제로 동작하는 UI(카드·표·차트·폼)를 스트리밍으로 그려내게 만드는 풀스택 제너러티브 UI(Generative UI) 프레임워크다. 핵심은 자체 설계한 압축 언어 OpenUI Lang인데, 이 언어는 JSON 대비 최대 67% 적은 토큰으로 같은 UI를 표현하도록 만들어졌다.
thesysdev/openui · 제작사 Thesys Inc. · 라이선스 MIT · 코어 @openuidev/lang-core 버전 0.2.10(패키지별 독립 버전 관리, 루트 package.json의 "1.0.1"은 실질적 진실원이 아님) · pnpm 모노레포, 11개 배포 패키지 · 소스 TS/TSX 약 730개 파일 · 테스트 파일 31개(Vitest) · 최신 커밋 9c72eef, 2026-07-29 · 동명이인 주의: wandb/openui와는 완전히 다른 프로젝트)
OpenUI를 한 줄로 정의하면 이렇다. "내가 미리 정의해둔 React(또는 Vue·Svelte) 컴포넌트 목록을, LLM이 OpenUI Lang이라는 압축 언어로 호출하게 만들고, 그 호출을 실시간으로 파싱해 화면에 그려주는 프레임워크." 단순히 "AI가 만든 컴포넌트 모음집"이 아니라, 언어 설계 + 파서 + 런타임 평가기 + 프레임워크별 렌더러를 자체 구현한 하나의 작은 프로그래밍 언어 생태계에 가깝다.
가장 헷갈리기 쉬운 지점부터 짚자면, OpenUI는 "AI 코드 생성 도구"가 아니다. 임의의 HTML/JSX를 즉석에서 만들어내는 게 아니라, 개발자가 defineComponent()로 미리 등록해둔 제한된 컴포넌트 집합 안에서만 LLM이 조립하도록 강제한다. 이 "울타리 쳐진 창의성(bounded generation)"이 이 프로젝트 설계의 핵심 축이다.
LLM에게 "아무 재료나 써서 집을 지어봐"(자유 HTML 생성)라고 하면 이상한 결과가 나올 수 있다. OpenUI는 대신 정해진 레고 부품 상자(Card, Table, BarChart, Form 같은 컴포넌트)를 건네고, 그 부품들의 이름·모양·조합 규칙을 담은 설명서(시스템 프롬프트)를 라이브러리 정의에서 자동으로 뽑아 LLM에게 준다.
LLM은 이 설명서를 보고 OpenUI Lang이라는 짧은 조립 지시문을 한 줄씩 스트리밍으로 뱉고, 그 지시문이 도착하는 즉시 화면에서 레고 블록이 하나씩 끼워 맞춰지듯 UI가 나타난다.
챗봇이 텍스트만 출력하던 시절을 지나, 이제는 차트·표·폼처럼 실제로 조작 가능한 UI를 LLM이 직접 그리게 하는 흐름이 Vercel의 AI SDK, Google의 A2UI, CopilotKit 등 여러 진영에서 동시에 나타나고 있다. OpenUI는 이 경쟁에서 "프레임워크(React/Vue/Svelte 전부 지원) + 토큰 효율(자체 언어)"이라는 두 축을 함께 파는 포지션을 잡았다.
기존 제너러티브 UI 도구 대부분은 LLM에게 JSON으로 UI 트리를 출력하게 한다. JSON은 사람이 읽기엔 좋지만, 중괄호·따옴표·반복되는 키 이름 때문에 토큰(모델이 텍스트를 처리하는 단위)을 많이 잡아먹는다. README에 실린 벤치마크(tiktoken GPT-5 인코더, 7가지 UI 시나리오 합산)에서 OpenUI Lang은 총 4,800토큰인 반면 Vercel의 JSON 렌더 방식은 10,180토큰, Thesys 자체의 구 JSON 포맷(C1 JSON)은 9,948토큰이 필요했다 — 각각 52.8%, 51.7% 절감이다. 토큰이 줄면 스트리밍 지연(latency)도 그만큼 짧아진다.
React가 메인이지만, @openuidev/vue-lang(Vue 3)과 @openuidev/svelte-lang(Svelte 5)이 같은 @openuidev/lang-core 위에서 동작한다. 여기에 빌드 도구 없이 <script> 태그 하나로 붙일 수 있는 @openuidev/browser-bundle까지 있어, "React 앱에만 종속되지 않는다"는 점이 채택 장벽을 낮춘다.
OpenUI는 완전한 오픈소스지만, 제작사 Thesys는 이 위에 OpenUI Cloud라는 매니지드 백엔드를 별도로 판다(대화 이력 저장, 프로덕션급 컴포넌트, 리포트/슬라이드 아티팩트, 화이트라벨 테마, 모델 폴백·관측성). "오픈소스로 채택 장벽을 낮추고, 운영 난이도가 높은 부분만 유료화"하는 전형적인 오픈코어(open-core) 사업 모델이 성장의 배경이다. 자세한 내용은 9번 섹션에서 다룬다.
LLM이 UI를 직접 만들게 하려는 모든 시도는 결국 같은 벽에 부딪힌다. 모델은 텍스트를 "예측"할 뿐이지, 문법적으로 완벽한 구조를 "보장"하지 않는다. 스트리밍 중간에 잘린 문장, 존재하지 않는 컴포넌트 이름, 타입이 안 맞는 값이 실시간으로 쏟아질 수 있다. OpenUI가 실제로 푸는 문제는 세 가지로 요약된다.
JSON은 구조화는 되지만 토큰이 비싸고, 자유 텍스트/HTML은 싸지만 구조가 없다. OpenUI Lang은 id = TypeName(위치인자1, 위치인자2, ...)라는 따옴표와 중괄호를 최소화한 한 줄 대입문 문법으로 이 사이를 메운다. 인자 이름 대신 위치(positional)로만 값을 넘겨 토큰을 더 줄인다.
토큰이 한 글자씩 도착하는 도중에도 화면은 계속 갱신돼야 한다. 자체 스트리밍 호환 파서(createStreamingParser)가 아직 닫히지 않은 문장을 자동으로 닫아보고(autoClose), meta.incomplete 플래그로 "이 결과는 아직 미완성"임을 알려준다. 완성된 부분만 먼저 그리고, 나머지는 다음 청크를 기다린다.
존재하지 않는 컴포넌트를 부르거나, 필수 인자가 빠지거나, 렌더링 중 예외가 나도 직전까지 정상적으로 그려졌던 화면을 그대로 유지해야 사용자 경험이 깨지지 않는다. React 렌더러의 ElementErrorBoundary가 "마지막으로 성공한 렌더링을 계속 보여주는" 방식으로 이 문제를 해결한다. 자세한 동작은 5번 섹션에서 코드로 확인한다.
공식 문서(docs/content/docs/openui-lang/overview.mdx)는 OpenUI를 "네 가지 핵심 블록"이라 설명하지만, 실제 코드를 따라가면 런타임 평가 단계가 파서와 렌더러 사이에 별도로 존재해 사실상 5단이다. 이 저장소가 무엇을 "직접 구현"하는지 감을 잡으려면 이 파이프라인을 먼저 그려보는 게 가장 빠르다.
이 구조가 주는 교훈은 "프레임워크(React/Vue/Svelte)에 종속된 로직은 최대한 얇게, 언어·파서·평가는 프레임워크 무관 코어에"라는 분리다. @openuidev/lang-core에는 React import가 단 한 줄도 없다. 이 덕분에 같은 파서·평가 로직을 세 프레임워크가 그대로 재사용할 수 있다.
OpenUI 저장소에는 LLM 자체가 없다. 실제 추론은 OpenAI·Anthropic·Google 등 외부 모델 제공자가 하고, OpenUI는 그 모델에게 "어떻게 응답할지"를 알려주는 프롬프트와, 그 응답을 "어떻게 그릴지"를 담당하는 파서·렌더러다.
또한 문서에 나오는 @openuidev/thesys, @openuidev/thesys-server 같은 OpenUI Cloud 전용 클라이언트 패키지는 이 오픈소스 저장소 안에 없다. packages/ 디렉터리를 직접 확인해도 11개 패키지 중 이 둘은 존재하지 않는다 — Cloud 연동 SDK는 별도로 배포되는 상용 구성요소라는 뜻이다. 9번 섹션에서 더 자세히 다룬다.
이 섹션이 이 문서에서 가장 중요하다. OpenUI의 진짜 값어치는 README의 "67% 절감" 숫자가 아니라, 그 숫자를 가능하게 만드는 네 가지 구체적인 설계 결정에 있다. 실제 소스 경로와 함수명을 인용하며 하나씩 뜯어본다.
개발자는 packages/lang-core/src/library.ts의 defineComponent()에 이름·설명·Zod 4 스키마·렌더러만 넘긴다. 여기서 흥미로운 함정 방지 코드가 있다 — assertV4Schema()가 Zod 3 스키마를 감지하면 "OpenUI requires Zod 4 schemas"라는 에러를 즉시 던진다. Zod 3과 4는 내부 구조(_def vs _zod)가 달라, 잘못된 버전을 쓰면 조용히 깨지는 대신 여기서 바로 걸러낸다.
그 다음 buildSignature()가 Zod 스키마의 필드 타입을 재귀적으로 순회해 "Card(children: Component[], title?: string)" 같은 사람이 읽는 함수 시그니처 문자열로 바꾼다. 이 문자열이 그대로 시스템 프롬프트에 박혀 LLM에게 "이 컴포넌트는 이런 인자를 받는다"고 알려준다 — 별도의 문서를 손으로 쓸 필요가 없다는 뜻이다. 실제로 예제 앱이 생성한 examples/openui-chat/src/generated/system-prompt.txt를 열어보면 BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", ...)처럼 이 자동 생성 시그니처들이 그대로 나열돼 있다.
스키마는 계약서, 시그니처는 그 계약서의 요약본이다. 개발자가 Zod로 "이 컴포넌트는 이런 값을 받는다"는 계약서를 한 번 작성해두면, OpenUI가 그 계약서를 읽고 LLM이 이해할 수 있는 짧은 요약(함수 시그니처)으로 자동 번역해 건네준다. 계약서를 두 번 쓸 필요가 없다.
실제 문법은 이렇다. 한 줄에 하나씩 식별자 = 표현식 형태로 값을 선언하고, root라는 특별한 식별자가 최종 렌더링 대상이 된다. 컴포넌트 호출은 TypeName(인자1, 인자2, ...)처럼 키워드 인자가 아닌 위치 인자로만 쓴다 — 실제로 시스템 프롬프트에는 "Write Stack([children], "row", "l") NOT Stack([children], direction: "row", gap: "l") — colon syntax is NOT supported and silently breaks"라는 경고가 명시돼 있다. 콜론 문법을 쓰면 "에러가 나는 게 아니라 조용히 깨진다"는 게 실전에서 중요한 함정이다.
# 실제 examples/openui-chat 생성 프롬프트에서 발췌한 예시 문법
header = CardHeader("월간 매출 요약")
chart = BarChart(["1월","2월","3월"], [Series("매출", [120, 150, 90])])
root = Card([header, chart])
또한 모든 변수는 부모에서 참조돼야만 렌더링된다(시스템 프롬프트 원문: "EVERY variable... MUST be referenced by at least one other variable. Unreferenced variables are silently dropped"). 이는 스트리밍 도중 어떤 문장이 아직 "고아 상태"(어디에도 안 붙은 상태)인지를 파서가 orphaned 필드로 추적하는 이유이기도 하다.
parser/statements.ts의 autoClose()와 split()은 아직 도착하지 않은 나머지 텍스트를 기다리지 않고, 지금까지 온 텍스트만으로 최선의 파싱 결과를 만들어낸다. parser/parser.ts의 classifyStatement()는 각 문장을 state($변수 선언) · query(Query(...) 호출) · mutation(Mutation(...) 호출) · value(일반 값)로 분류하는데, 특히 $foo = Query(...)처럼 겉보기엔 상태 선언인데 실제로는 쿼리인 경우를 Query 호출 여부부터 먼저 검사해 올바르게 분류한다 — 코드 주석에도 "check BEFORE $var to handle `$foo = Query(...)` correctly"라고 명시돼 있다.
파싱 결과에는 언제나 meta.incomplete(아직 스트림이 끝나지 않았다는 표시), meta.unresolved(아직 값이 안 채워진 참조), meta.orphaned(부모에 연결되지 않은 문장)이 함께 따라온다. 렌더러는 이 메타데이터를 보고 "지금 완성된 부분까지만" 그린다.
단순히 정적인 UI만 그리는 게 아니라, LLM이 Query(도구이름, 인자, 기본값, 새로고침조건) 형태로 실제 백엔드 함수나 MCP(Model Context Protocol) 도구를 호출하게 할 수 있다. runtime/queryManager.ts의 createQueryManager()가 이 호출을 관리하고, runtime/mcp.ts는 MCP 클라이언트의 응답 포맷(callTool 결과)을 OpenUI가 이해하는 값으로 변환하는 extractToolResult()를 제공한다. React 쪽에서는 Renderer의 toolProvider prop이 "함수 맵" 또는 "MCP 클라이언트" 둘 중 하나를 그대로 받아들이도록 설계돼, OpenUI 자체가 MCP 표준을 흉내내지 않고 그 위에 얹힌다.
react-lang/src/Renderer.tsx의 ElementErrorBoundary는 일반적인 React 에러 바운더리와 다르게 동작한다. 에러가 나면 그냥 대체 UI를 보여주는 게 아니라, componentDidUpdate()에서 매번 this.lastValidChildren = this.props.children으로 마지막 정상 렌더링을 계속 저장해뒀다가, 에러 상태(hasError)일 땐 그 저장본을 그대로 반환한다. 코드 주석 그대로 옮기면: "이 '마지막으로 성공한 상태 보여주기' 동작은 스트리밍 중이나 일시적 평가 오류 중에 UI가 하얗게 비는 것을 막아준다." 스트리밍 특유의 "아직 다 안 왔는데 지금 이 순간엔 문법이 깨져 보이는" 상태를 사용자에게 노출하지 않는 실전적인 해법이다.
lang-core(파서·평가·프롬프트)는 순수 TypeScript로만 짜여 있고, React/Vue/Svelte 각각은 그 위에 "컴포넌트 정의 API + 렌더 트리 순회"만 얇게 얹는다. 하나의 언어 사양을 세 프레임워크가 공유하는 이 구조는, 여러 프론트엔드 기술 스택을 동시에 지원해야 하는 SDK를 설계할 때 참고할 만한 패턴이다.
루트 package.json의 이름은 "OpenUI", 버전은 "1.0.1"로 적혀 있지만 이 값은 배포되는 실제 패키지와 무관한 장식용 필드다. 실제로 npm에 올라가는 것은 packages/* 아래 11개 패키지이며, 각자 독립적으로 버전이 매겨진다. 즉 이 프로젝트의 "버전 진실원"은 루트가 아니라 각 패키지 폴더의 package.json이다.
| 패키지 | 버전 | 역할 |
|---|---|---|
lang-core | 0.2.10 | 프레임워크 무관 파서·프롬프트 생성·런타임 평가·타입 (코어) |
react-lang | 0.2.9 | React용 defineComponent·createLibrary·<Renderer/>·훅 |
react-headless | 0.9.3 | 비주얼 컴포넌트 없이 채팅 상태·스트리밍 어댑터만 제공 |
react-ui | 0.13.1 | 기본 제공 컴포넌트 라이브러리 2종 + AgentInterface 채팅 UI |
react-email | 0.2.4 | React Email 기반 이메일 생성용 컴포넌트 |
vue-lang | 0.1.3 | Vue 3 바인딩 |
svelte-lang | 0.1.2 | Svelte 5 바인딩 |
browser-bundle | 0.1.1 | 빌드 도구 없이 CDN/iframe에 붙이는 window.__OpenUI 번들 |
openui-cli(@openuidev/cli) | 0.2.1 | 프로젝트 스캐폴딩(create) + 프롬프트/스키마 생성(generate) |
devtools | 0.0.1 | 개발용 디버깅 도구(초기 단계, 버전 0.0.1) |
observability | 0.0.1 | 프레임워크 무관 이벤트 버스(초기 단계, 버전 0.0.1) |
버전 관리에는 흔한 changesets 도구(.changeset/ 폴더)가 보이지 않는다 — 즉 각 패키지 버전은 자동화된 릴리스 도구 없이 수동으로 관리되는 것으로 보인다. 공통 의존성은 pnpm-workspace.yaml의 catalog: 블록에 모아 버전 드리프트를 막는다. 핵심 값만 추리면:
| 의존성 | 버전 범위(catalog) | 비고 |
|---|---|---|
| react / react-dom | ^18.3.1 || ^19.0.0 | React 18과 19 모두 지원 (peerDependency) |
| zod | ^3.25.0 || ^4.0.0 | range 표기상 3.25+도 허용되지만, library.ts가 런타임에 Zod 4 내부 구조(_zod)를 요구 — 3.25는 "zod/v4" 서브패스로 임포트해야 함 |
| zustand | ^4.5.5 | react-headless의 채팅 상태 스토어에 사용 |
| typescript | ^5.9.3 | |
| @modelcontextprotocol/sdk | >=1.0.0(선택) | MCP 클라이언트 연동 시에만 필요, peerDependenciesMeta로 optional 처리 |
catalog의 zod 범위는 ^3.25.0 || ^4.0.0이라 언뜻 "Zod 3도 된다"고 읽힌다. 하지만 defineComponent()는 스키마 객체에 _zod 내부 필드가 없으면(순수 Zod 3 방식) 즉시 런타임 에러를 던진다. 실제로는 Zod 3.25 이상에서 "zod/v4" 서브패스로 import하거나 Zod 4 패키지를 써야 한다 — 그냥 zod에서 기본 import하면 예전 버전 사용자는 바로 걸린다.
| 항목 | 내용 |
|---|---|
| 가장 빠른 시작 | npx @openuidev/cli@latest create --name genui-chat-app → .env에 OPENAI_API_KEY 설정 → npm run dev. Next.js 기반 예제 앱이 그대로 생성됨 |
| 기존 프로젝트에 추가 | npm install @openuidev/react-lang @openuidev/react-ui (React) 또는 @openuidev/vue-lang / @openuidev/svelte-lang |
| 백엔드/엣지 전용 | npm install @openuidev/lang-core — UI 프레임워크 없이 프롬프트·스키마만 생성할 때 |
| 저장소 자체 빌드 | pnpm 워크스페이스. CI(build-js.yml)는 pnpm 9.0.6 + Node 20으로 pnpm install --frozen-lockfile 후 pnpm --filter "./packages/**" run build 실행 |
| Node 버전 | 루트 package.json에 engines 필드가 없어 강제되진 않음. 다만 pnpm-workspace.yaml의 catalog가 @types/node ^22.15.32를 지정해 개발 환경은 Node 22 기준으로 보임(CI 실행 환경인 Node 20과는 다르다는 점에 유의) |
| 테스트 | Vitest. 각 패키지 package.json의 test 스크립트로 개별 실행, 루트에서는 pnpm -r run test로 전체 실행. 저장소 전체 테스트 파일 31개(코어 파서/런타임에 집중, 예: lang-core/src/__tests__만 약 1,200줄) |
| CI 워크플로 | build-js.yml(빌드+ci 스크립트), cli-e2e.yml(CLI 스캐폴딩 종단 테스트), cli-template-package-managers.yml(npm/pnpm/yarn 등 패키지 매니저 호환성), publish-npm-package.yml, context7.yml |
처음이라면 @openuidev/react-ui가 내장한 두 라이브러리 중 하나로 시작한다. openuiLibrary(루트 컴포넌트 Stack, 레이아웃 자유도가 높은 범용 라이브러리)이거나 openuiChatLibrary(루트 컴포넌트 Card, 세로로 쌓이는 채팅 전용 라이브러리로 FollowUpBlock·ListBlock 같은 대화형 컴포넌트 포함). 직접 컴포넌트를 더할 땐 defineComponent() + Zod 스키마로 정의해 기존 라이브러리에 합친다.
library.prompt(promptOptions) 한 줄이면 등록된 모든 컴포넌트의 시그니처·문법 규칙·예시가 담긴 시스템 프롬프트 문자열이 만들어진다. 이 프롬프트를 백엔드에서 LLM 호출 시 system 메시지로 넘긴다. CLI의 openui generate 명령으로 빌드 타임에 미리 파일로 뽑아둘 수도 있다(예제 앱의 src/generated/system-prompt.txt가 이렇게 생성된 결과물).
사용자가 메시지를 보내면 앱 서버가 시스템 프롬프트 + 대화 기록을 LLM(OpenAI 등)에 보내고, 모델은 채팅 텍스트 대신 openui-lang 코드를 토큰 단위로 스트리밍 응답한다. 프런트엔드는 이 스트림을 그대로 누적한 문자열을 <Renderer response={"{streamedText}"} />에 매 청크마다 다시 넘긴다.
Renderer는 내부적으로 매 렌더마다 새 텍스트를 파싱하고(2단계), AST를 실제 값으로 평가하고(3단계), 결과 트리를 화면에 그린다(4단계). 사용자가 폼에 값을 입력하거나 버튼을 누르면 toolProvider로 등록해둔 함수(또는 MCP 클라이언트)가 Query()/Mutation() 호출을 실제로 실행하고, 그 결과가 다시 반응형 상태($변수)로 반영돼 UI가 갱신된다.
Query()/Mutation() 호출의 실행 백엔드로 MCP 클라이언트를 그대로 꽂아 쓸 수 있게 한다.wandb/openui는 이름만 같은 완전히 다른 프로젝트다검색하면 wandb/openui(Weights & Biases가 공개했던, 자연어 프롬프트 하나로 웹페이지 하나를 즉석 생성하는 데모형 웹앱)가 먼저 걸릴 수 있다. 이 문서가 다루는 것은 그것이 아니라 thesysdev/openui다. 접근 방식 자체가 반대에 가깝다.
wandb/openui는 "매번 자유 형식 HTML을 처음부터 새로 생성"하는 임의 코드 생성 데모였고, 사전 정의된 컴포넌트 스키마도, 재사용 가능한 파서·런타임도, npm에 배포되는 SDK도 없었다. 반면 thesysdev/openui는 미리 등록한 유한한 컴포넌트 집합만 조합하게 하는 언어·파서·런타임을 갖춘 SDK 제품군이다. 저장소 링크나 @openuidev/* 패키지명을 반드시 확인해야 한다.
공식 문서(docs/content/docs/openui-lang/overview.mdx)는 Library·Prompt Generator·Parser·Renderer라는 4개 블록만 설명한다. 하지만 실제 소스에는 파서와 렌더러 사이에 runtime/evaluator.ts·runtime/store.ts·runtime/queryManager.ts로 이뤄진 별도의 평가·상태·쿼리 실행 계층이 뚜렷이 존재한다(4번 섹션 참고). 문서만 보고 "파서가 끝나면 바로 그려진다"고 이해하면 Query()/Mutation()·$변수 반응형 상태가 어디서 처리되는지 놓치게 된다.
스킬 문서(skills/openui/SKILL.md)와 docs/content/docs/agent/getting-started/openui-cloud.mdx는 @openuidev/thesys·@openuidev/thesys-server 패키지의 API(useOpenuiCloudStorage(), artifactTool() 등)를 자세히 설명하지만, packages/ 디렉터리를 직접 열어보면 이 두 패키지의 소스는 존재하지 않는다. CLI가 생성하는 openui-cloud 템플릿(packages/openui-cli/src/templates/openui-cloud/)에서 이 패키지들을 사용하는 코드만 볼 수 있을 뿐, 구현체는 별도로 비공개 배포된다. "이 레포만 클론하면 Cloud 기능까지 전부 볼 수 있다"고 기대하면 안 된다.
LICENSE 파일에는 Copyright (c) 2011-2024 Thesys Inc.라고 적혀 있다. 최신 커밋은 2026-07-29인데 저작권 연도 상한이 2024로 멈춰 있고, 시작 연도 2011도 이 특정 프로젝트의 실제 시작 시점(더 최근으로 보임)과는 무관해 보인다 — 다른 프로젝트의 라이선스 보일러플레이트를 그대로 가져오면서 갱신을 놓친 사례로 보인다. 라이선스 조건(MIT) 자체에는 영향이 없지만, "이 숫자로 회사 설립 연도를 추정"하는 실수는 하지 말아야 한다.
시스템 프롬프트 규칙에 명시된 대로, OpenUI Lang은 Stack([children], direction: "row") 같은 이름 붙은 인자(named argument) 문법을 지원하지 않는다. 오직 위치 인자만 허용되며, 콜론 문법을 쓰면 파서가 에러를 던지는 대신 해당 문장을 조용히 누락시킨다. 커스텀 컴포넌트에 예시(examples)를 추가할 때 이 규칙을 프롬프트에 명확히 반복해주지 않으면 LLM이 자연스럽게 콜론 문법으로 새는 경우가 흔하다.
| 대안 | 접근 방식 | OpenUI와 다른 점 |
|---|---|---|
| Vercel AI SDK (json-render류) | LLM이 JSON 형태로 UI 트리·도구 호출을 출력, React로 매핑 | OpenUI 대비 토큰이 약 2배 이상(README 벤치마크 기준 10,180 vs 4,800). 다만 Vercel 생태계(Next.js, AI SDK)와의 통합은 더 매끄러움 |
| Google A2UI | JSON 기반, 모바일(iOS/Android)까지 포괄하는 커스텀 컴포넌트 전용 스펙 | JSON 기반이라 토큰 비용 구조가 비슷하게 큼. "커스텀 컴포넌트만" 지원해 미리 만든 컴포넌트 라이브러리를 재사용하기 어려움 |
| CopilotKit OpenGenUI | 코파일럿/에이전트 UI 프레임워크의 일부로 제너러티브 UI 제공 | README 비교표 기준 스트리밍이 "부분적(Partial)"이고 출력 일관성도 상대적으로 약함으로 소개됨(자사 벤치마크라 참고용) |
| MCP (Model Context Protocol) | LLM이 외부 도구·리소스를 호출하는 방식의 표준 | 경쟁 관계가 아니다. MCP는 "도구를 어떻게 부르나"를, OpenUI Lang은 "결과를 어떤 UI로 그리나"를 다룬다 — 실제로 Query()가 MCP 클라이언트를 직접 받아 그 위에서 동작한다 |
| Thesys C1 JSON (자사 구 포맷) | Thesys가 OpenUI 이전에 쓰던 자체 JSON 포맷 | 같은 회사의 전작 격으로, README 벤치마크에서 OpenUI Lang 대비 토큰을 약 2배(9,948 vs 4,800) 더 쓰는 것으로 나온다 — "우리도 JSON을 벗어나려고 자체 언어를 새로 설계했다"는 방향성을 보여줌 |
| 자유 HTML/JSX 생성 (예: 초기 방식들) | 스키마 없이 LLM이 매번 임의의 마크업을 생성 | 유연하지만 타입 안전성·재사용성·보안(임의 스크립트 실행 위험)에서 불리. OpenUI는 "미리 등록된 컴포넌트만" 허용해 이 위험을 원천 차단 |
반대로 프레임워크 종속 없이 순수 JSON 스펙만 필요하거나, 이미 Vercel AI SDK/CopilotKit 생태계에 깊게 들어가 있다면 굳이 새 언어를 배울 이유는 적다. "토큰 절감 폭"은 README 자체 벤치마크 수치이므로, 실제 채택 전엔 자신의 UI 시나리오로 재현해보는 게 안전하다(benchmarks/ 폴더에 재현 스크립트가 그대로 포함돼 있다).
OpenUI는 "챗봇에 카드·차트·폼처럼 진짜 조작 가능한 UI를 붙이고 싶은데, JSON 방식의 토큰 비용과 스트리밍 어색함이 싫다"는 구체적인 니즈를 겨냥한 프레임워크다. 실제 코드를 뜯어보면 마케팅 문구("67% 토큰 절감")보다 더 인상적인 건, Zod 스키마 하나로 문서·타입·검증을 자동 생성하는 라이브러리 계층과 스트리밍 중 미완성 상태를 명시적으로 다루는 파서, 그리고 에러가 나도 화면을 지우지 않는 렌더러다.
· AI 채팅/코파일럿 제품에 표·차트·폼 같은 실제 조작 가능한 UI를 붙이려는 팀
· React 외에 Vue·Svelte 등 여러 프론트엔드 스택을 함께 지원해야 하는 SDK/플랫폼 팀
· 스트리밍 지연·토큰 비용에 민감해 JSON 기반 방식의 한계를 이미 체감한 팀
· 오픈소스로 먼저 검증하고, 필요하면 Thesys의 OpenUI Cloud로 프로덕션 하드닝(관측성·모델 폴백·화이트라벨)만 나중에 붙이고 싶은 팀
· 순수 텍스트 챗봇이면 이 프레임워크의 이점을 거의 못 씀
· 완전히 자유로운 임의 코드(HTML/JS)를 그대로 실행하고 싶다면, "제한된 컴포넌트만 허용"하는 OpenUI의 설계 철학과 정면으로 반대됨
· 이미 Vercel AI SDK나 CopilotKit 생태계에 깊게 결합돼 있어 새 언어를 배우는 비용이 이점보다 크다면 굳이 바꿀 이유는 적음
· Cloud SDK(@openuidev/thesys*)의 실제 구현까지 코드로 확인하고 싶다면, 이 저장소만으로는 부족함(9번 섹션 참고)