packages/core의 스키마·카탈로그·스트리밍 엔진, 30여 개 렌더러 패키지의 설계, AI SDK 연동 코드까지 직접 뜯어보고 "이 레포에서 무엇을 배울 수 있는가"에 초점을 맞췄다.이 레포가 정확히 무엇을 하는 물건인가.
LLM에게 "HTML/JSX 코드를 직접 작성해줘"라고 하면 결과는 매번 다르고, 깨진 마크업·존재하지 않는 컴포넌트·보안 위험(XSS)이 섞일 수 있다. json-render는 그 대신 당신이 정의한 컴포넌트 카탈로그(Card, Button, Metric…)를 LLM에게 알려주고, LLM은 그 부품들로 구성한 JSON 트리(spec)만 생성하게 한다. 당신의 앱은 그 JSON을 미리 만들어 둔 안전한 컴포넌트로 렌더링한다.
즉 흐름은 ① 프롬프트 → ② AI + 카탈로그 → ③ JSON spec → ④ 렌더러의 4단계다. AI의 출력은 "코드"가 아니라 데이터(JSON)이기 때문에, 검증(validate)·스트리밍·재현이 가능하고, React Native·PDF·이메일·3D·터미널 등 같은 JSON을 여러 플랫폼에서 재사용할 수 있다.
// AI가 생성하는 것은 코드가 아니라 이런 "평면 spec" JSON
{
"root": "card-1",
"elements": {
"card-1": { "type": "Card", "props": { "title": "안녕" }, "children": ["btn-1"] },
"btn-1": { "type": "Button", "props": { "label": "클릭" }, "children": [] }
}
}
왜 Vercel Labs가 이걸 내고, 트렌딩에 올랐는가.
2024~2025년 "AI가 만드는 UI"는 큰 화두였다. Vercel의 AI SDK에는 이미 generative UI(툴 호출로 React 컴포넌트를 스트리밍) 기능이 있었지만, 그건 개발자가 코드로 컴포넌트를 미리 짜 두고 AI가 그중 하나를 고르는 방식에 가까웠다. json-render는 한 발 더 나아가 AI 출력 자체를 검증 가능한 데이터 명세로 표준화하고, 그 명세를 거의 모든 렌더 타깃으로 확장했다. 트렌딩 이유를 셋으로 정리한다.
AI는 카탈로그에 정의된 컴포넌트와 액션만 쓸 수 있다. 존재하지 않는 태그를 만들거나 임의 코드를 실행할 수 없다. 출력은 항상 당신의 스키마(zod로 정의)에 맞는 JSON이라, "AI가 이상한 걸 그렸다"는 사고를 구조적으로 차단한다.
같은 spec JSON을 React·Vue·Svelte·Solid(웹), React Native(모바일), Remotion(영상), react-pdf(문서), react-email(이메일), Ink(터미널), R3F(3D), Image(OG 카드)로 렌더한다. UI를 "코드"가 아닌 "데이터"로 다루기에 가능한 일이다.
LLM이 토큰을 흘리는 동안 SpecStream 컴파일러가 부분 JSON을 조금씩 받아 화면을 점진적으로 그린다(progressive rendering). 사용자는 빈 화면을 기다리지 않고 UI가 채워지는 걸 본다. 그러면서도 최종 결과는 스키마에 맞게 예측 가능하다.
| 구분 | "AI가 코드 직접 생성" | json-render (JSON spec) |
|---|---|---|
| 출력물 | JSX/HTML 코드(문자열) | 검증 가능한 JSON 데이터 |
| 안전성 | 임의 코드·XSS 위험 | 카탈로그로 가드레일 |
| 재현성 | 매번 다름 | 스키마로 고정 |
| 플랫폼 | 보통 한 곳(웹) | 웹·모바일·PDF·영상·터미널… |
| 스트리밍 | 까다로움 | SpecStream 내장 |
① 표현력의 천장. AI는 당신이 만든 부품으로만 조립하므로, 카탈로그에 없는 UI는 절대 못 만든다(그게 핵심 안전장치이자 동시에 제약). ② 카탈로그 설계 비용. 좋은 결과는 좋은 카탈로그(컴포넌트·props 스키마·description)에서 나온다 — 초기 설계 품질이 곧 결과 품질이다. ③ 신생 프로젝트. v0.x대로 API가 바뀔 수 있고, 에코시스템·예제가 아직 성장 중이다.
코어 / 렌더러 / 상태·도구 / 빌드·인프라로 나눠 본다.
json-render는 pnpm + Turborepo 모노레포다. 30개가 넘는 npm 패키지(@json-render/*)가 한 저장소에 들어 있고, 모두 @json-render/core 위에 얹힌다. 핵심은 "코어는 프레임워크를 모른다"는 점 — core는 순수 TypeScript + zod로 스키마·검증·스트리밍만 담당하고, React/Vue/Svelte 의존성은 각 렌더러 패키지가 가진다.
| 기술 | 역할 |
|---|---|
| TypeScript 5.9 | 전 패키지 타입 안전성. 카탈로그→props 타입 추론까지 제네릭으로 구현 |
| Zod 4 | 컴포넌트 props 스키마 정의·런타임 검증. 카탈로그의 진실원 |
| 스키마 빌더 | defineSchema/defineCatalog — spec 형태와 카탈로그가 제공할 것을 선언 |
| SpecStream | createSpecStreamCompiler — 부분 JSON 청크를 점진적 spec으로 컴파일 |
| Directives | $state/$cond/$template/$computed 등 동적 표현식 해석 엔진 |
| 패키지 | 타깃 |
|---|---|
| react / vue / svelte / solid | 웹 UI (4대 프레임워크) |
| react-native | 모바일 (iOS/Android) |
| shadcn / shadcn-svelte | 36종 사전 제작 shadcn/ui 컴포넌트 |
| remotion | 영상 (타임라인 spec) |
| react-pdf / react-email | PDF 문서 / HTML 이메일 |
| ink | 터미널 TUI |
| image | Satori 기반 SVG/PNG (OG 이미지) |
| react-three-fiber | 3D 씬 (가우시안 스플랫 포함) |
| next | Next.js 풀 앱 (라우트·레이아웃·SSR·메타데이터) |
| 패키지 | 역할 |
|---|---|
| redux / zustand / jotai / xstate | 외부 상태 라이브러리를 StateStore에 어댑터로 연결 |
| devtools(-react/vue/svelte/solid) | 드롭인 인스펙터: spec 트리·상태 편집·액션 로그·DOM 피커 |
| directives | $format·$math·$concat·$t(i18n) 등 즉시 쓰는 7+종 디렉티브 |
| mcp | MCP Apps 연동 — Claude·ChatGPT·Cursor·VS Code에 UI 서빙 |
| yaml | JSON 대신 YAML 와이어 포맷(스트리밍 파서·편집 모드) |
| codegen | UI 트리에서 실제 코드 생성 |
| 기술 | 역할 |
|---|---|
| pnpm 11 + Turborepo | 모노레포 의존성·태스크 캐싱 빌드 |
| tsup | 각 패키지 ESM/CJS 동시 번들 |
| Vitest 4 | 단위 테스트(코어에만 수십 개 *.test.ts) |
| Vercel AI SDK (ai) | 예제의 LLM 연동 — pipeJsonRender로 스트림 변환 |
| Next.js (apps/web) | 문서 사이트 + 플레이그라운드(MDX) |
json-render의 구조는 "공용 어댑터 + 멀티 콘센트"와 같다. core는 표준 전압(JSON spec)을 만드는 발전기이고, 각 렌더러는 그 표준 전기를 나라별 콘센트(React·Vue·PDF·영상…)에 맞게 꽂아주는 어댑터다. 새 플랫폼이 필요하면 어댑터(렌더러 패키지) 하나만 더 만들면 된다.
프롬프트가 화면이 되기까지, 그리고 핵심 설계 패턴.
전체 파이프라인은 단순하지만 각 단계가 명확히 분리돼 있다. 아래 그림은 "사용자 입력 → LLM → JSON spec → 렌더러 → 화면"의 흐름과, 그 안에서 가드레일·스트리밍·상태가 어디서 작동하는지를 보여준다.
가장 영리한 부분. 개발자가 defineCatalog로 컴포넌트·props·설명을 선언하면, catalog.prompt()가 그걸 읽어 LLM용 시스템 프롬프트를 자동 생성한다. "어떤 부품이 있고 props는 무엇인지"를 사람이 두 번 적을 필요가 없다. 카탈로그 = 진실원(single source of truth).
UI 트리를 중첩 객체로 두지 않고 elements 맵 + children ID 배열로 평탄화했다. 스트리밍 중 부분 파싱이 쉽고, 특정 노드만 패치(부분 갱신)하기 좋다. 이것이 SpecStream의 newPatches와 맞물린다.
props 값은 고정 데이터일 수도, {"$state":"/path"}·{"$cond":…} 같은 표현식일 수도 있다. 렌더러는 렌더 시점에 이 디렉티브를 상태(StateStore) 기준으로 해석한다. JSON 안에 "선언적 로직"을 담되, 실행은 안전한 해석기가 한다.
상태는 StateStore 인터페이스 뒤에 숨겨 redux·zustand·jotai·xstate 무엇이든 끼울 수 있고, 렌더는 core 위에 프레임워크별 패키지를 얹는다. 코어는 어떤 UI 라이브러리도 import하지 않는다 — 의존성 역전의 모범.
setState로 상태를 바꾸면, 그 상태를 읽는 visible 조건과 $cond props가 자동 재평가돼 화면이 갱신된다. 아래는 탭 전환 UI가 JSON만으로 동작하는 예다.{
"type": "Pressable",
"props": {
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
}
}
// 다른 노드: 상태 값에 따라 아이콘이 자동으로 바뀜
{
"type": "Icon",
"props": {
"name": { "$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home", "$else": "home-outline" }
}
}
모노레포의 어디에 무엇이 있는가.
루트는 pnpm 워크스페이스 설정과 Turborepo 파이프라인이고, 실제 코드는 packages/(라이브러리), apps/web(문서 사이트), examples/(데모), skills/(AI 에이전트용 사용법)로 나뉜다.
| 처음 본다면 읽을 순서 | 이유 |
|---|---|
| README.md → packages/core/src/schema.ts | 전체 개념 + 카탈로그/스키마의 핵심 |
| examples/no-ai | LLM 없이 spec→렌더만 보는 가장 단순한 예 |
| examples/chat + app/api/generate/route.ts | 실제 AI 연동(스트리밍) 전체 흐름 |
| packages/react/src/renderer.tsx | spec 트리가 컴포넌트로 변환되는 실물 코드 |
| skills/core/SKILL.md | 제작자가 정리한 "올바른 사용법" 요약 |
이 레포를 뜯어보며 실제로 손에 남기면 좋은 것들.
core의 카탈로그는 zod 스키마로 컴포넌트 props를 정의하고, 그 스키마에서 ① 타입(개발자용) ② 런타임 검증 ③ LLM 프롬프트(JSON Schema 설명) 세 가지를 동시에 뽑아낸다. "한 번 정의, 세 곳에서 활용"하는 설계는 어떤 데이터 중심 앱에서도 강력한 패턴이다.
// 카탈로그: props 스키마 + 설명이 곧 AI에게 주는 명세
const catalog = defineCatalog(schema, {
components: {
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
format: z.enum(["currency","percent","number"]).nullable(),
}),
description: "지표 값 하나를 표시", // ← LLM이 읽는 힌트
},
},
actions: { refresh_data: { description: "모든 지표 새로고침" } },
});
const systemPrompt = catalog.prompt(); // 프롬프트 자동 생성
LLM이 토큰을 흘리는 중간에는 JSON이 닫히지 않은 "부서진" 상태다. createSpecStreamCompiler는 이런 부분 청크를 받아 지금까지 만들어진 만큼의 spec을 돌려준다. 스트리밍 LLM UI를 만들 때 핵심이 되는 기술 — 직접 구현하면 까다로운 부분을 라이브러리가 해결한 사례를 코드로 배울 수 있다.
const compiler = createSpecStreamCompiler();
for await (const chunk of stream) {
const { result, newPatches } = compiler.push(chunk);
setSpec(result); // 부분 결과로 즉시 화면 갱신
}
const finalSpec = compiler.getResult();
@json-render/core는 React/Vue를 전혀 모른다. 대신 StateStore·렌더러 같은 인터페이스만 정의하고, 구체 구현은 바깥(react·zustand 패키지)에서 주입한다. 라이브러리를 프레임워크에 묶이지 않게 설계하는 정석을 실물로 볼 수 있다.
examples/chat의 API 라우트는 Vercel AI SDK의 스트림을 pipeJsonRender로 감싸 클라이언트에 보낸다. AI Gateway에 모델을 문자열 ID("anthropic/claude-haiku-4.5")로 넘기는 최신 패턴도 함께 익힌다.
// app/api/generate/route.ts (요지)
const result = await agent.stream({ messages: modelMessages });
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
30개 패키지에 압도되지 말 것. core + react 두 패키지만 깊게 보면 전체 그림이 잡힌다. renderer.tsx에서 spec 트리를 도는 로직과 props.ts의 동적 값 해석만 이해하면, vue/svelte 렌더러는 같은 개념의 번역본임을 알게 된다.
개발·실행에 필요한 환경.
라이브러리 자체는 가볍지만, 모노레포 전체를 빌드하려면 비교적 최신 툴체인이 필요하다. 루트 package.json의 engines가 이를 강제한다(engineStrict: true).
| 항목 | 요구/권장 | 비고 |
|---|---|---|
| Node.js | >= 24 | engines로 강제(엄격 모드) |
| pnpm | >= 11 (11.1.3 고정) | packageManager 필드로 고정 |
| 패키지 매니저 | pnpm 워크스페이스 | npm/yarn으로는 모노레포 빌드 비권장 |
| 빌드 도구 | Turborepo · tsup | 패키지별 ESM/CJS 번들 |
| portless | 로컬 dev에 필요 | *.localhost 서브도메인 라우팅 |
| LLM 키(예제) | AI_GATEWAY_API_KEY | chat 예제·e2e 테스트용 |
단지 라이브러리를 쓰기만 할 거라면 요구사항은 훨씬 낮다 — 일반적인 React/Vue 프로젝트에 npm install @json-render/core @json-render/react로 추가하면 끝이다. peerDependency로 zod와 해당 프레임워크가 필요하다.
pnpm 설정에 engineStrict: true가 켜져 있어, Node 버전이 낮으면 pnpm install 단계에서 거부된다. nvm 등으로 Node 24+를 먼저 맞추고, corepack enable로 pnpm 11을 활성화한 뒤 진행하라.
"읽기"에서 "만들기"로 — 난이도별 5단계.
examples/no-ai를 실행해 LLM 없이 손으로 쓴 spec JSON이 화면이 되는 걸 본다. root/elements 구조를 바꿔보며 트리가 어떻게 매핑되는지 감을 잡는다.
defineCatalog로 Card/Button 외에 새 컴포넌트(예: Rating)를 zod props와 함께 추가하고, defineRegistry로 실제 React 구현을 등록한다. catalog.prompt() 출력이 어떻게 바뀌는지 확인한다.
setState 액션 버튼과 $cond/$state를 써서, JSON spec만으로 탭 전환 또는 다크모드 토글을 구현한다. 코드 없이 데이터로 인터랙션을 표현하는 핵심을 체득한다.
examples/chat을 띄우고 카탈로그를 내 것으로 교체해, "OO 대시보드 만들어줘" 프롬프트로 실제 LLM이 내 컴포넌트로 UI를 생성하게 한다. 스트리밍이 점진적으로 렌더되는 걸 관찰한다.
한 spec을 react-pdf(문서)나 image(OG 카드)로도 렌더해 본다. "UI = 데이터"의 위력을 직접 확인하고, 타깃별 카탈로그 차이를 비교한다. 여유가 되면 devtools를 붙여 spec 트리를 인스펙트한다.
json-render를 제대로 활용하기 위한 4주 코스.
| 주차 | 주제 | 도달 목표 |
|---|---|---|
| 1주차 | Zod + TypeScript 제네릭 | 스키마로 타입·검증 동시 확보, 카탈로그 구조 이해 |
| 2주차 | json-render core + react | 카탈로그·레지스트리·렌더러 직접 구성, 동적 디렉티브 |
| 3주차 | Vercel AI SDK + 스트리밍 | streamText/Object, pipeJsonRender, SpecStream 점진 렌더 |
| 4주차 | 상태 어댑터 + 멀티 타깃 | zustand/jotai 연결, pdf·image 등 다른 렌더러 적용 |
| 이후 | MCP Apps / Next 풀앱 | Claude·Cursor에 UI 서빙, JSON으로 라우트·SSR까지 |
이 레포의 진짜 가치는 "AI 기능"보다 "UI를 데이터로 다루는 설계 사고"에 있다. 1~2주차의 스키마 주도 설계 + 의존성 역전은 json-render를 쓰지 않더라도 평생 써먹는 패턴이다. AI 연동(3주차)은 그 위에 얹는 응용일 뿐이니, 기초 설계부터 탄탄히 보는 걸 권한다.
코드를 읽다 막히는 용어 빠른 참조.
| Generative UI | AI가 자연어 요청에 맞춰 인터랙티브 UI를 생성하는 패러다임. json-render의 정체성. |
| Catalog(카탈로그) | AI가 쓸 수 있는 컴포넌트·액션의 목록과 props 스키마. 가드레일이자 진실원. |
| Spec(명세) | AI가 생성하는 결과물. root + elements 맵으로 된 평면 UI 트리 JSON. |
| Registry(레지스트리) | 카탈로그의 각 컴포넌트 타입을 실제 프레임워크 컴포넌트로 매핑한 것. |
| Renderer(렌더러) | spec을 받아 트리를 순회하며 실제 화면(React/Vue/PDF…)으로 그리는 엔진. |
| defineCatalog | 스키마 + 컴포넌트/액션 정의로 카탈로그를 만드는 core 함수. |
| catalog.prompt() | 카탈로그에서 LLM용 시스템 프롬프트를 자동 생성. |
| SpecStream | 스트리밍 LLM의 부분 JSON을 점진적 spec으로 컴파일하는 유틸. |
| Directive(디렉티브) | $state·$cond·$template·$computed 등 JSON 안의 동적 표현식. |
| StateStore | 상태 모델 추상 인터페이스. redux/zustand/jotai/xstate로 교체 가능. |
| setState | 상태 모델을 직접 갱신하는 내장 액션. visible·동적 props를 재평가시킴. |
| watch | 특정 상태가 바뀌면 액션을 트리거하는 element 상의 감시자. |
| Zod | TS용 스키마 선언·런타임 검증 라이브러리. props 정의의 기반. |
| Turborepo / pnpm | 모노레포 빌드 캐싱 / 워크스페이스 패키지 매니저. |
| MCP Apps | Model Context Protocol로 Claude·Cursor 등에 UI를 서빙하는 방식. |