본문으로 건너뛰기
5분 AI.
REPO DEEP DIVE · 2026-07-29 · THESYSDEV/OPENUI · JSON 대신 "streaming-first 언어"로 UI를 그리는 프레임워크

thesysdev/openui 딥다이브
— "LLM이 UI를 직접 그리는" 스트리밍 전용 언어와 렌더러

프레임워크

빠른 판단

무엇
LLM이 전용 언어로 컴포넌트를 호출하면 스트리밍 파싱해 렌더링하는 생성형 UI 프레임워크
이럴 때
AI 채팅에 정해진 카드·차트·폼을 붙이고 모델 출력이 사전에 정의한 컴포넌트로 표시되게 할 때
OpenUI(패키지명 @openuidev/*)는 LLM(거대 언어 모델)이 채팅 답변 대신 실제로 동작하는 UI(카드·표·차트·폼)를 스트리밍으로 그려내게 만드는 풀스택 제너러티브 UI(Generative UI) 프레임워크다. 핵심은 자체 설계한 압축 언어 OpenUI Lang인데, 이 언어는 JSON 대비 최대 67% 적은 토큰으로 같은 UI를 표현하도록 만들어졌다.

이 저장소는 컴포넌트 레지스트리(등록만 해두는 카탈로그)가 아니라, "컴포넌트 정의 → 시스템 프롬프트 생성 → LLM 스트림 파싱 → 값 평가 → React/Vue/Svelte 렌더링"까지 이어지는 4단 파이프라인(Library → Prompt → Parser → Renderer)을 직접 구현한 런타임이다.

(저장소 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와는 완전히 다른 프로젝트)
목차
  1. 한 줄 정체
  2. 왜 지금 뜨는가
  3. 무엇을 해결하나
  4. 전체 구조
  5. 핵심 설계·동작 원리
  6. 기술 스택 · 의존성
  7. 설치 · 요구사항
  8. 실제 사용 흐름
  9. 함정 · 오해 방지
  10. 대안 비교
  11. 정리 · 누가 쓰면 좋은가
  12. 직접 해볼 수 있는 실습 과제
  13. 관련 기술 심화 학습 로드맵
  14. 핵심 키워드 사전

1한 줄 정체

"컴포넌트 카탈로그 + 전용 언어 + 스트리밍 파서 + 렌더러"를 통째로 갖춘 제너러티브 UI 런타임

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가 나타난다.

2왜 지금 뜨는가

"AI가 텍스트만 뱉는 시대"에서 "AI가 UI를 그리는 시대"로 — 토큰 비용과 프레임워크 종속의 두 벽을 동시에 공략

① "제너러티브 UI(Generative UI)"라는 장르 자체가 막 커지는 시점

챗봇이 텍스트만 출력하던 시절을 지나, 이제는 차트·표·폼처럼 실제로 조작 가능한 UI를 LLM이 직접 그리게 하는 흐름이 Vercel의 AI SDK, Google의 A2UI, CopilotKit 등 여러 진영에서 동시에 나타나고 있다. OpenUI는 이 경쟁에서 "프레임워크(React/Vue/Svelte 전부 지원) + 토큰 효율(자체 언어)"이라는 두 축을 함께 파는 포지션을 잡았다.

② JSON 기반 방식의 "토큰 비용" 문제를 정면으로 겨냥

기존 제너러티브 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번 섹션에서 다룬다.

3무엇을 해결하나

"LLM 출력 = 신뢰할 수 없는 문자열"이라는 근본 문제를 스키마·파서·에러 복구로 감싼다

LLM이 UI를 직접 만들게 하려는 모든 시도는 결국 같은 벽에 부딪힌다. 모델은 텍스트를 "예측"할 뿐이지, 문법적으로 완벽한 구조를 "보장"하지 않는다. 스트리밍 중간에 잘린 문장, 존재하지 않는 컴포넌트 이름, 타입이 안 맞는 값이 실시간으로 쏟아질 수 있다. OpenUI가 실제로 푸는 문제는 세 가지로 요약된다.

문제 ①

"토큰을 아끼면서" 구조화된 출력을 받을 방법이 없다

JSON은 구조화는 되지만 토큰이 비싸고, 자유 텍스트/HTML은 싸지만 구조가 없다. OpenUI Lang은 id = TypeName(위치인자1, 위치인자2, ...)라는 따옴표와 중괄호를 최소화한 한 줄 대입문 문법으로 이 사이를 메운다. 인자 이름 대신 위치(positional)로만 값을 넘겨 토큰을 더 줄인다.

문제 ②

스트리밍 중간에 output이 "아직 안 끝난 상태"를 어떻게 다루나

토큰이 한 글자씩 도착하는 도중에도 화면은 계속 갱신돼야 한다. 자체 스트리밍 호환 파서(createStreamingParser)가 아직 닫히지 않은 문장을 자동으로 닫아보고(autoClose), meta.incomplete 플래그로 "이 결과는 아직 미완성"임을 알려준다. 완성된 부분만 먼저 그리고, 나머지는 다음 청크를 기다린다.

문제 ③

모델이 실수해도 화면이 하얗게 깨지면 안 된다

존재하지 않는 컴포넌트를 부르거나, 필수 인자가 빠지거나, 렌더링 중 예외가 나도 직전까지 정상적으로 그려졌던 화면을 그대로 유지해야 사용자 경험이 깨지지 않는다. React 렌더러의 ElementErrorBoundary가 "마지막으로 성공한 렌더링을 계속 보여주는" 방식으로 이 문제를 해결한다. 자세한 동작은 5번 섹션에서 코드로 확인한다.

용어
제너러티브 UI (Generative UI)
LLM이 순수 텍스트 답변이 아니라, 버튼·표·차트·폼처럼 실제로 조작 가능한 UI 구성 요소를 만들어내는 방식. "AI가 그린 화면"이라고 생각하면 된다.
용어
토큰 (token)
LLM이 글을 처리하는 최소 단위(대략 단어 조각). 토큰 수가 많을수록 비용과 지연 시간이 커진다. OpenUI Lang은 같은 UI를 표현할 때 JSON보다 토큰을 덜 쓰도록 설계됐다.

4전체 구조

Library → Prompt Generator → Parser → Runtime → Renderer, 5단으로 이어지는 파이프라인

공식 문서(docs/content/docs/openui-lang/overview.mdx)는 OpenUI를 "네 가지 핵심 블록"이라 설명하지만, 실제 코드를 따라가면 런타임 평가 단계가 파서와 렌더러 사이에 별도로 존재해 사실상 5단이다. 이 저장소가 무엇을 "직접 구현"하는지 감을 잡으려면 이 파이프라인을 먼저 그려보는 게 가장 빠르다.

┌────────────────────────────────────────────────────────────────────┐ │ OPENUI 파이프라인 (lang-core 중심) │ └────────────────────────────────────────────────────────────────────┘ [0] LIBRARY defineComponent() + createLibrary() · Zod 스키마로 컴포넌트 props 정의 + React/Vue/Svelte 렌더러 등록 · library.ts: buildSignature() → "Card(children: Component[], title?: string)" │ ▼ [1] PROMPT GENERATOR parser/prompt.ts: generatePrompt() / generateSystemPrompt() · 라이브러리의 컴포넌트 시그니처 + 문법 규칙 + 예시를 시스템 프롬프트 문자열로 조립 · "@openuidev/cli generate" 로 CLI에서도 동일 로직 실행 가능 │ (시스템 프롬프트를 LLM에 전달) ▼ ┌───────────────┐ │ LLM │ OpenUI Lang 텍스트를 토큰 단위로 스트리밍 응답 └───────────────┘ │ (한 글자씩 도착하는 openui-lang 텍스트) ▼ [2] PARSER lexer.ts → statements.ts → parser.ts · tokenize() → split()/autoClose()(미완성 문장 자동 닫기) → parse() · classifyStatement()로 각 줄을 state/query/mutation/value로 분류 · meta.incomplete / unresolved / orphaned 로 "아직 미완성" 상태 추적 │ (ParseResult: 타입이 붙은 AST + 상태/쿼리/뮤테이션 목록) ▼ [3] RUNTIME runtime/evaluator.ts, store.ts, queryManager.ts · evaluate(): AST를 실제 값(문자열/숫자/객체/컴포넌트 트리)으로 평가 · createStore(): $변수(반응형 상태) 보관 + 구독 · createQueryManager(): Query()/Mutation() 호출을 실제 도구(tool)/MCP로 연결 │ (평가된 ElementNode 트리) ▼ [4] RENDERER react-lang/Renderer.tsx (Vue·Svelte도 동형 구조) · library.components[node.typeName] 로 실제 컴포넌트를 찾아 매핑 · ElementErrorBoundary: 에러 시 "마지막으로 성공한 렌더링"을 계속 표시 │ ▼ 화면에 점진적으로(streaming) 그려지는 실제 UI

이 구조가 주는 교훈은 "프레임워크(React/Vue/Svelte)에 종속된 로직은 최대한 얇게, 언어·파서·평가는 프레임워크 무관 코어에"라는 분리다. @openuidev/lang-core에는 React import가 단 한 줄도 없다. 이 덕분에 같은 파서·평가 로직을 세 프레임워크가 그대로 재사용할 수 있다.

중요한 경계
이 저장소 ≠ "AI 모델" · 이 저장소 ≠ Thesys의 상용 백엔드

OpenUI 저장소에는 LLM 자체가 없다. 실제 추론은 OpenAI·Anthropic·Google 등 외부 모델 제공자가 하고, OpenUI는 그 모델에게 "어떻게 응답할지"를 알려주는 프롬프트와, 그 응답을 "어떻게 그릴지"를 담당하는 파서·렌더러다.

또한 문서에 나오는 @openuidev/thesys, @openuidev/thesys-server 같은 OpenUI Cloud 전용 클라이언트 패키지는 이 오픈소스 저장소 안에 없다. packages/ 디렉터리를 직접 확인해도 11개 패키지 중 이 둘은 존재하지 않는다 — Cloud 연동 SDK는 별도로 배포되는 상용 구성요소라는 뜻이다. 9번 섹션에서 더 자세히 다룬다.

5핵심 설계 · 동작 원리

Zod 스키마 → 타입 시그니처 자동 생성 · 스트리밍 파서의 "미완성 허용" · Query/Mutation 도구 호출 · 에러에도 안 깨지는 렌더러

이 섹션이 이 문서에서 가장 중요하다. OpenUI의 진짜 값어치는 README의 "67% 절감" 숫자가 아니라, 그 숫자를 가능하게 만드는 네 가지 구체적인 설계 결정에 있다. 실제 소스 경로와 함수명을 인용하며 하나씩 뜯어본다.

설계 ① — Zod 스키마 한 번 쓰면, 컴포넌트 문서·타입·검증이 전부 자동 생성

개발자는 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이 이해할 수 있는 짧은 요약(함수 시그니처)으로 자동 번역해 건네준다. 계약서를 두 번 쓸 필요가 없다.

설계 ② — OpenUI Lang: "따옴표·중괄호를 줄인" 위치 기반 대입문 언어

실제 문법은 이렇다. 한 줄에 하나씩 식별자 = 표현식 형태로 값을 선언하고, 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(부모에 연결되지 않은 문장)이 함께 따라온다. 렌더러는 이 메타데이터를 보고 "지금 완성된 부분까지만" 그린다.

설계 ④ — Query()/Mutation(): LLM이 실제 데이터·도구를 부르는 통로 (MCP와도 연결)

단순히 정적인 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를 설계할 때 참고할 만한 패턴이다.

6기술 스택 · 의존성

pnpm 모노레포 · Zod 4 · React 18/19 · Vue 3 · Svelte 5 · 버전의 "진실원"은 각 패키지 package.json

루트 package.json의 이름은 "OpenUI", 버전은 "1.0.1"로 적혀 있지만 이 값은 배포되는 실제 패키지와 무관한 장식용 필드다. 실제로 npm에 올라가는 것은 packages/* 아래 11개 패키지이며, 각자 독립적으로 버전이 매겨진다. 즉 이 프로젝트의 "버전 진실원"은 루트가 아니라 각 패키지 폴더의 package.json이다.

패키지버전역할
lang-core0.2.10프레임워크 무관 파서·프롬프트 생성·런타임 평가·타입 (코어)
react-lang0.2.9React용 defineComponent·createLibrary·<Renderer/>·훅
react-headless0.9.3비주얼 컴포넌트 없이 채팅 상태·스트리밍 어댑터만 제공
react-ui0.13.1기본 제공 컴포넌트 라이브러리 2종 + AgentInterface 채팅 UI
react-email0.2.4React Email 기반 이메일 생성용 컴포넌트
vue-lang0.1.3Vue 3 바인딩
svelte-lang0.1.2Svelte 5 바인딩
browser-bundle0.1.1빌드 도구 없이 CDN/iframe에 붙이는 window.__OpenUI 번들
openui-cli(@openuidev/cli)0.2.1프로젝트 스캐폴딩(create) + 프롬프트/스키마 생성(generate)
devtools0.0.1개발용 디버깅 도구(초기 단계, 버전 0.0.1)
observability0.0.1프레임워크 무관 이벤트 버스(초기 단계, 버전 0.0.1)

버전 관리에는 흔한 changesets 도구(.changeset/ 폴더)가 보이지 않는다 — 즉 각 패키지 버전은 자동화된 릴리스 도구 없이 수동으로 관리되는 것으로 보인다. 공통 의존성은 pnpm-workspace.yaml의 catalog: 블록에 모아 버전 드리프트를 막는다. 핵심 값만 추리면:

의존성버전 범위(catalog)비고
react / react-dom^18.3.1 || ^19.0.0React 18과 19 모두 지원 (peerDependency)
zod^3.25.0 || ^4.0.0range 표기상 3.25+도 허용되지만, library.ts가 런타임에 Zod 4 내부 구조(_zod)를 요구 — 3.25는 "zod/v4" 서브패스로 임포트해야 함
zustand^4.5.5react-headless의 채팅 상태 스토어에 사용
typescript^5.9.3
@modelcontextprotocol/sdk>=1.0.0(선택)MCP 클라이언트 연동 시에만 필요, peerDependenciesMeta로 optional 처리
버전 함정
"Zod 4"라고만 알고 있으면 반쯤 틀린다

catalog의 zod 범위는 ^3.25.0 || ^4.0.0이라 언뜻 "Zod 3도 된다"고 읽힌다. 하지만 defineComponent()는 스키마 객체에 _zod 내부 필드가 없으면(순수 Zod 3 방식) 즉시 런타임 에러를 던진다. 실제로는 Zod 3.25 이상에서 "zod/v4" 서브패스로 import하거나 Zod 4 패키지를 써야 한다 — 그냥 zod에서 기본 import하면 예전 버전 사용자는 바로 걸린다.

7설치 · 요구사항

CLI 스캐폴딩이 기본 경로 · Node 22 개발환경(CI는 Node 20) · pnpm 9.0.6 고정
항목내용
가장 빠른 시작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

8실제 사용 흐름

라이브러리 정의 → 프롬프트 생성 → LLM 스트림 → <Renderer/> 렌더링, 4단계로 실제 앱을 그려보기
단계 1

컴포넌트 라이브러리 고르기 또는 정의하기

처음이라면 @openuidev/react-ui가 내장한 두 라이브러리 중 하나로 시작한다. openuiLibrary(루트 컴포넌트 Stack, 레이아웃 자유도가 높은 범용 라이브러리)이거나 openuiChatLibrary(루트 컴포넌트 Card, 세로로 쌓이는 채팅 전용 라이브러리로 FollowUpBlock·ListBlock 같은 대화형 컴포넌트 포함). 직접 컴포넌트를 더할 땐 defineComponent() + Zod 스키마로 정의해 기존 라이브러리에 합친다.

단계 2

라이브러리에서 시스템 프롬프트 생성

library.prompt(promptOptions) 한 줄이면 등록된 모든 컴포넌트의 시그니처·문법 규칙·예시가 담긴 시스템 프롬프트 문자열이 만들어진다. 이 프롬프트를 백엔드에서 LLM 호출 시 system 메시지로 넘긴다. CLI의 openui generate 명령으로 빌드 타임에 미리 파일로 뽑아둘 수도 있다(예제 앱의 src/generated/system-prompt.txt가 이렇게 생성된 결과물).

단계 3

LLM 응답을 스트리밍으로 받기

사용자가 메시지를 보내면 앱 서버가 시스템 프롬프트 + 대화 기록을 LLM(OpenAI 등)에 보내고, 모델은 채팅 텍스트 대신 openui-lang 코드를 토큰 단위로 스트리밍 응답한다. 프런트엔드는 이 스트림을 그대로 누적한 문자열을 <Renderer response={"{streamedText}"} />에 매 청크마다 다시 넘긴다.

단계 4

렌더러가 파싱·평가·그리기를 한 번에 처리, 사용자가 폼을 조작하면 Query/Mutation이 다시 발화

Renderer는 내부적으로 매 렌더마다 새 텍스트를 파싱하고(2단계), AST를 실제 값으로 평가하고(3단계), 결과 트리를 화면에 그린다(4단계). 사용자가 폼에 값을 입력하거나 버튼을 누르면 toolProvider로 등록해둔 함수(또는 MCP 클라이언트)가 Query()/Mutation() 호출을 실제로 실행하고, 그 결과가 다시 반응형 상태($변수)로 반영돼 UI가 갱신된다.

용어
MCP (Model Context Protocol)
LLM이 외부 도구·데이터에 접근하는 방식을 표준화한 프로토콜(Anthropic 주도). OpenUI는 MCP를 대체하지 않고, Query()/Mutation() 호출의 실행 백엔드로 MCP 클라이언트를 그대로 꽂아 쓸 수 있게 한다.

9함정 · 오해 방지

동명이인 wandb/openui 완전 별개 · Cloud SDK 미포함 · 라이선스 저작 연도 등 자잘한 함정
가장 중요한 함정
wandb/openui는 이름만 같은 완전히 다른 프로젝트다

검색하면 wandb/openui(Weights & Biases가 공개했던, 자연어 프롬프트 하나로 웹페이지 하나를 즉석 생성하는 데모형 웹앱)가 먼저 걸릴 수 있다. 이 문서가 다루는 것은 그것이 아니라 thesysdev/openui다. 접근 방식 자체가 반대에 가깝다.

wandb/openui는 "매번 자유 형식 HTML을 처음부터 새로 생성"하는 임의 코드 생성 데모였고, 사전 정의된 컴포넌트 스키마도, 재사용 가능한 파서·런타임도, npm에 배포되는 SDK도 없었다. 반면 thesysdev/openui는 미리 등록한 유한한 컴포넌트 집합만 조합하게 하는 언어·파서·런타임을 갖춘 SDK 제품군이다. 저장소 링크나 @openuidev/* 패키지명을 반드시 확인해야 한다.

문서 vs 실제 코드 불일치
"4가지 블록"이라는 공식 설명은 실제로는 5단 파이프라인이다

공식 문서(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()·$변수 반응형 상태가 어디서 처리되는지 놓치게 된다.

문서 vs 실제 코드 불일치
"OpenUI Cloud" 관련 패키지는 이 저장소 안에 없다

스킬 문서(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 파일의 저작권 연도가 저장소 나이와 안 맞는다

LICENSE 파일에는 Copyright (c) 2011-2024 Thesys Inc.라고 적혀 있다. 최신 커밋은 2026-07-29인데 저작권 연도 상한이 2024로 멈춰 있고, 시작 연도 2011도 이 특정 프로젝트의 실제 시작 시점(더 최근으로 보임)과는 무관해 보인다 — 다른 프로젝트의 라이선스 보일러플레이트를 그대로 가져오면서 갱신을 놓친 사례로 보인다. 라이선스 조건(MIT) 자체에는 영향이 없지만, "이 숫자로 회사 설립 연도를 추정"하는 실수는 하지 말아야 한다.

문법 함정
"콜론 문법"을 쓰면 에러 없이 조용히 깨진다

시스템 프롬프트 규칙에 명시된 대로, OpenUI Lang은 Stack([children], direction: "row") 같은 이름 붙은 인자(named argument) 문법을 지원하지 않는다. 오직 위치 인자만 허용되며, 콜론 문법을 쓰면 파서가 에러를 던지는 대신 해당 문장을 조용히 누락시킨다. 커스텀 컴포넌트에 예시(examples)를 추가할 때 이 규칙을 프롬프트에 명확히 반복해주지 않으면 LLM이 자연스럽게 콜론 문법으로 새는 경우가 흔하다.

10대안 비교

JSON 기반 진영(Vercel, Google, CopilotKit) · MCP는 경쟁자가 아니라 이웃 · Thesys 구 포맷(C1 JSON)은 자사 전작
대안접근 방식OpenUI와 다른 점
Vercel AI SDK
(json-render류)
LLM이 JSON 형태로 UI 트리·도구 호출을 출력, React로 매핑OpenUI 대비 토큰이 약 2배 이상(README 벤치마크 기준 10,180 vs 4,800). 다만 Vercel 생태계(Next.js, AI SDK)와의 통합은 더 매끄러움
Google A2UIJSON 기반, 모바일(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는 "미리 등록된 컴포넌트만" 허용해 이 위험을 원천 차단
선택 기준 요약
"이미 React/Vue/Svelte 앱이 있고, 토큰 비용·스트리밍 품질이 중요하다면" OpenUI가 유리한 자리

반대로 프레임워크 종속 없이 순수 JSON 스펙만 필요하거나, 이미 Vercel AI SDK/CopilotKit 생태계에 깊게 들어가 있다면 굳이 새 언어를 배울 이유는 적다. "토큰 절감 폭"은 README 자체 벤치마크 수치이므로, 실제 채택 전엔 자신의 UI 시나리오로 재현해보는 게 안전하다(benchmarks/ 폴더에 재현 스크립트가 그대로 포함돼 있다).

11정리 · 누가 쓰면 좋은가

구조화된 AI UI가 필요한 프로덕트 팀 · 여러 프론트엔드 프레임워크를 함께 지원해야 하는 SDK 팀

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번 섹션 참고)

12직접 해볼 수 있는 실습 과제

문자열 파싱부터 실제 렌더링과 도구 실행까지, 외부 모델 없이 네 단계로 확인하기

완성된 채팅 앱부터 실행하면 모델 응답과 화면 처리 중 어디서 문제가 생겼는지 구분하기 어렵다. 먼저 고정된 OpenUI Lang 문자열과 가짜 도구로 각 단계를 따로 확인해 보자. 아래 경로와 명령은 확인한 로컬 소스 기준이며, 의존성과 워크스페이스 패키지 빌드가 준비된 학습용 사본의 레포 루트에서 실행한다. Node와 pnpm 설정은 해당 사본의 package.json과 pnpm-workspace.yaml을 먼저 확인한다.

테스트 추가와 변경은 학습용 사본에서 진행한다. 다음 환경 변수는 코어 패키지의 원격 측정을 끄는 설정이며, 아래 과제는 API 키나 실제 모델 호출을 사용하지 않는다.

export OPENUI_TELEMETRY_DISABLED=1
과제 1 · 입문

나중에 도착한 제목을 화면 트리에 연결하기

목표: 참조 이름만 먼저 도착한 상태와 실제 값까지 도착한 상태를 구분한다.

  1. packages/lang-core/src/parser/__tests__/parser.test.ts에서 schema와 전방 참조 테스트를 읽는다. 이 파일의 createStreamParser()는 이미 준비된 인자 정보를 받는 내부 진입점이다.
  2. 기존 가져오기와 schema를 재사용해 아래 테스트를 같은 파일에 추가한다. 첫 번째 push()에는 부모 문장만, 두 번째에는 제목 정의만 넣는다.
  3. 제목 정의 뒤에 unused = Title("연결되지 않은 제목")도 보내는 변형을 추가하고, meta.orphaned에 unused가 들어가는지 확인한다.
it("나중에 도착한 제목을 연결한다", () => {
  const stream = createStreamParser(schema);
  const first = stream.push("root = Stack([heading])\n");
  expect(first.meta.unresolved).toContain("heading");

  const last = stream.push('heading = Title("학습 기록")\n', true);
  expect(last.meta.unresolved).toEqual([]);
  expect(last.root?.props.children).toMatchObject([
    { typeName: "Title", props: { text: "학습 기록" } },
  ]);
});

다음 명령은 코어 패키지의 test 스크립트에 이 테스트 파일만 전달한다.

pnpm --filter @openuidev/lang-core run test src/parser/__tests__/parser.test.ts

완료 기준: 첫 결과에는 미해결 참조가 남고, 마지막 결과에는 제목 노드가 연결된다. 연결하지 않은 정의는 고아 문장으로 구분되며, 추가한 테스트가 통과한다.

과제 2 · 중급

지표 컴포넌트 하나로 프롬프트와 입력 계약 만들기

목표: 컴포넌트의 입력 정의가 모델용 설명과 파서용 스키마로 이어지는 과정을 확인한다.

  1. packages/lang-core/src/__tests__/library.test.ts에서 defineComponent(), createLibrary(), toJSONSchema() 사용례를 읽는다. 이 파일은 zod/v4의 z와 createParser()를 이미 가져온다.
  2. 같은 파일에 아래 테스트를 추가한다. 여기서 component: () => null은 입력 계약만 시험하기 위한 빈 구현이며, 실제 화면 그리기는 다음 과제에서 붙인다.
  3. 입력을 root = Metric("매출", "많음")으로 바꾼 테스트도 추가한다. meta.errors에 type-mismatch가 있는지 검사하고, 생성된 스키마의 $defs.Metric.properties에서 두 필드의 타입을 확인한다.
it("지표의 설명과 파싱 결과를 함께 확인한다", () => {
  const Metric = defineComponent({
    name: "Metric",
    description: "이름과 숫자를 보여주는 지표",
    props: z.object({ label: z.string(), value: z.number() }),
    component: () => null,
  });
  const library = createLibrary({ components: [Metric], root: "Metric" });

  expect(library.prompt()).toContain("Metric(label: string, value: number)");
  const parser = createParser(library.toJSONSchema(), library.root);
  const result = parser.parse('root = Metric("매출", 120)');
  expect(result.meta.errors).toEqual([]);
  expect(result.root?.props.value).toBe(120);
});
pnpm --filter @openuidev/lang-core run test src/__tests__/library.test.ts

완료 기준: 필드 순서가 프롬프트 시그니처에 반영되고, 숫자 입력은 파싱되며 잘못된 타입은 오류로 기록된다. 스키마를 바꾸면 설명과 검증에 어떤 영향이 생기는지도 설명할 수 있다.

과제 3 · 중급

같은 지표를 React 요소로 그리고 값 바꾸기

목표: 파싱된 값이 컴포넌트의 props를 거쳐 화면의 글자로 바뀌는 연결을 확인한다.

  1. packages/react-lang/src/Renderer.test.tsx의 기존 테스트를 읽는다. 이 파일은 jsdom이라는 가상 브라우저 환경에서 createRoot()와 act()로 React를 실행한다.
  2. 이 파일이 가져오는 React용 defineComponent()로 Metric을 다시 정의한다. 과제 2와 같은 입력 스키마를 쓰고, 렌더링 함수만 아래처럼 바꾼다. 별도의 metricLibrary에 등록해 기존 폼 테스트와 함께 둔다.
  3. 기존 테스트의 컨테이너 생성과 act() 구조를 따라 새 테스트를 작성한다. <Renderer library={metricLibrary} response={'root = Metric("매출", 120)'} />를 그린 뒤, 같은 React 루트에 값이 150인 응답을 다시 전달한다.
  4. 각 렌더링 뒤 container.textContent를 검사하고, 마지막에 기존 예제처럼 root.unmount()를 호출한다.
component: ({ props }) => (
  <p>{props.label}: {props.value}</p>
)
pnpm --filter @openuidev/react-lang run test src/Renderer.test.tsx

완료 기준: 첫 결과는 매출: 120, 다음 결과는 매출: 150으로 확인된다. 기존 폼 테스트도 통과하고, packages/react-lang/src/library.ts와 Renderer.tsx에서 입력 정의와 그리기의 담당 지점을 찾을 수 있다.

과제 4 · 심화

가짜 도구로 자동 새로고침의 시작과 종료 검증하기

목표: 도구 호출 결과뿐 아니라 타이머 정리와 재활성화까지 검증한다.

  1. packages/lang-core/src/runtime/queryManager.ts와 packages/lang-core/src/runtime/__tests__/queryManager.test.ts를 읽는다. 테스트의 node()는 쿼리 정보를 만들고, flushMicrotasks()는 비동기 결과가 반영될 때까지 기다린다.
  2. 같은 테스트 파일에서 vi.fn().mockResolvedValue({ total: 120 })을 callTool로 쓰는 사례를 추가한다. createQueryManager({ callTool })를 만들고 evaluateQueries([node("q1", 5)])로 시작한 뒤 결과를 확인한다.
  3. 기존 vi.useFakeTimers() 설정에서 vi.advanceTimersByTimeAsync(5_000)으로 시간을 전진시킨다. 비동기 작업을 비운 뒤 호출 횟수가 늘어나는지 확인한다.
  4. dispose() 뒤에는 시간을 전진시켜도 횟수가 늘지 않는지 확인한다. 이어서 activate()와 같은 쿼리의 evaluateQueries()를 호출하고, 다시 한 주기를 전진시켜 자동 호출이 재개되는지 검사한다. 끝나면 dispose()로 정리한다.
pnpm --filter @openuidev/lang-core run test src/runtime/__tests__/queryManager.test.ts

완료 기준: getResult("q1")에서 가짜 결과를 읽을 수 있고, 활성화 중에는 호출 횟수가 증가하며 종료 뒤에는 멈춘다. 재활성화하면 같은 새로고침 간격에서도 호출이 다시 이어지고, 테스트가 실제 시간 대기 없이 통과한다.

13관련 기술 심화 학습 로드맵

입력 계약 → 언어 해석 → 화면 → 상태 → 도구 연결 순서로 쌓는 5주 과정

매주 코드를 읽고 작은 결과물을 하나 남긴다. 다음 단계로 넘어갈 때는 함수 이름을 외우기보다 어떤 입력이 들어와 어떤 결과로 바뀌는지를 설명해 보자. 아래 파일 경로는 모두 레포 루트 기준이다.

주차배울 개념과 읽을 소스남길 결과물
1주차 TypeScript 객체 타입, Zod 입력 스키마, 위치 인자와 시그니처의 관계.
packages/lang-core/src/library.ts, packages/lang-core/src/signature.ts, packages/lang-core/src/__tests__/library.test.ts
과제 2의 지표 정의와 정상·잘못된 타입 입력 테스트. 스키마 필드, 프롬프트 시그니처, 파싱된 값의 대응표.
2주차 문자열을 토큰과 문장으로 나누기, 전방 참조, 미완성 문장 처리.
packages/lang-core/src/parser/lexer.ts, statements.ts, parser.ts와 같은 폴더의 __tests__/parser.test.ts
과제 1의 조각별 파싱 결과 기록. push()에 새 조각을 넣는 경우와 set()에 누적 전체를 넣는 경우의 비교.
3주차 공통 코어와 React 바인딩의 역할 분리, 컴포넌트 등록, 렌더링 오류 격리.
packages/react-lang/src/library.ts, packages/react-lang/src/Renderer.tsx, packages/react-lang/src/Renderer.test.tsx
과제 3의 지표 렌더링 테스트. OpenUI Lang 문자열이 Metric의 화면 글자로 바뀌는 흐름을 파일명과 함께 그린 그림.
4주차 반응형 상태, 구독, 스트리밍 기본값과 사용자 입력의 관계.
packages/lang-core/src/runtime/store.ts, 같은 폴더의 __tests__/store.test.ts, packages/react-lang/src/hooks/useStateField.ts
기본값 초기화 → 사용자 값 변경 → 새 파싱 결과 적용 순서의 상태 기록. 사용자가 바꾼 값이 유지되는 조건을 검증하는 테스트.
5주차 비동기 도구 호출, 캐시, 타이머 수명 관리, MCP 응답 변환.
packages/lang-core/src/runtime/queryManager.ts, packages/lang-core/src/runtime/mcp.ts, examples/agent-frameworks/vercel-ai-sdk/
과제 4의 호출 횟수 검증과 도구 결과 흐름도. 예제의 src/app/page.tsx와 src/app/api/chat/route.ts를 읽고 클라이언트와 서버의 연결 경계를 표시한 메모.

마지막 주의 예제는 소스 추적 자료로 사용한다. 먼저 가짜 도구 테스트로 실행 흐름을 이해하고, 예제의 서버 경로에서는 외부 모델 호출이 어느 지점에 놓이는지만 확인하면 된다.

14핵심 키워드 사전

부품 정의부터 도구 호출까지, 소스를 읽을 때 다시 찾아볼 열다섯 가지 말
용어
제너러티브 UI
질문에 대한 답을 버튼·표·카드 같은 조작 가능한 화면으로 구성하는 방식이다. 이 레포에서는 등록된 컴포넌트를 OpenUI Lang으로 조합하고 Renderer가 실제 화면으로 그린다.
용어
도메인 전용 언어와 OpenUI Lang
특정 일을 간결하게 표현하도록 만든 작은 언어를 도메인 전용 언어라고 한다. 이 레포의 OpenUI Lang은 root = Metric("매출", 120)처럼 컴포넌트 조합과 값을 표현하며, packages/lang-core/src/parser/에서 해석한다.
용어
스키마와 Zod
스키마는 입력값의 모양을 적은 검사표이고, Zod는 그 검사표를 코드로 표현하는 도구다. defineComponent()의 props에 스키마를 넘기면 이 레포가 시그니처와 파서용 스키마를 만드는 데 사용한다.
용어
컴포넌트 라이브러리
화면을 조립할 때 사용할 부품과 각 부품의 입력 규칙을 모은 상자다. 이 레포의 createLibrary()는 컴포넌트들을 묶어 prompt(), toJSONSchema()와 렌더링에 필요한 등록 정보를 제공한다.
용어
위치 인자
값의 이름 대신 전달한 순서로 의미를 구분하는 방식이다. 과제의 Metric("매출", 120)에서는 첫 값이 label, 다음 값이 value에 대응하므로 스키마 필드 순서와 호출 순서를 함께 읽어야 한다.
용어
시그니처
이름 뒤에 받을 값의 순서와 타입을 적은 짧은 사용 설명서다. 이 레포는 스키마에서 Metric(label: string, value: number) 같은 설명을 만들며, 관련 로직은 packages/lang-core/src/signature.ts에 있다.
용어
시스템 프롬프트
모델에게 사용할 부품과 출력 문법을 알려주는 작업 지침이다. 이 레포의 library.prompt()는 등록 정보와 예시 등을 문자열로 조립하며, 생성 과정은 packages/lang-core/src/parser/prompt.ts에서 확인할 수 있다.
용어
스트리밍 파서
텍스트가 조금씩 도착할 때마다 현재까지의 구조를 해석하는 처리기다. 공개 API인 createStreamingParser()로 만들며, push()는 새 조각을 추가하고 set()은 누적된 전체 텍스트를 받는다.
용어
전방 참조
값이 정의되기 전에 그 이름부터 사용하는 것이다. 이 레포에서는 부모가 먼저 참조한 이름을 meta.unresolved로 추적하고, 뒤의 조각에서 정의가 도착하면 트리에 연결한다.
용어
추상 구문 트리
텍스트에 적힌 식의 구조를 객체와 가지로 표현한 중간 결과다. 이 레포의 packages/lang-core/src/parser/ast.ts가 관련 형태를 정의하고, packages/lang-core/src/runtime/evaluator.ts가 식을 실제 값으로 평가한다.
용어
반응형 상태
값이 바뀌면 이를 구독하는 화면도 갱신할 수 있도록 보관하는 상태다. 이 레포에서는 $로 시작하는 변수와 폼 입력을 다루며, createStore()와 React의 useStateField()가 상태 저장과 접근을 담당한다.
용어
Query와 Mutation
Query()는 데이터를 읽는 도구 호출이고 Mutation()은 변경 작업을 요청하는 도구 호출이다. 이 레포의 createQueryManager()는 조회 결과·캐시와 변경 호출 상태를 관리한다.
용어
도구 실행 공급자
도구 이름과 인자를 받아 실제 함수를 실행해 주는 연결 부품이다. React의 Renderer에는 toolProvider로 함수 모음이나 MCP 클라이언트를 넘길 수 있고, 내부 쿼리 관리자는 이를 callTool() 형태로 사용한다.
용어
MCP
모델과 애플리케이션이 외부 도구를 호출할 때 사용하는 공통 통신 규약이다. 이 레포의 packages/lang-core/src/runtime/mcp.ts는 extractToolResult()로 도구 응답의 구조화된 데이터나 텍스트를 꺼낸다.
용어
에러 바운더리
하위 컴포넌트의 렌더링 예외를 잡아 화면 전체로 번지는 것을 막는 경계다. 이 레포의 ElementErrorBoundary는 packages/react-lang/src/Renderer.tsx에 있으며, 오류가 발생하면 마지막으로 성공한 자식 렌더링을 반환하도록 설계돼 있다.