프론트엔드는 한때 고정된 것이었다. 디자이너가 그리고 엔지니어가 만들면, 사용자는 그냥 배포된 것을 사용했다.
그건 끝났다.
2026 년에 배포되는 인터페이스는 에이전트 자체가 사용자의 요청에 따라 실시간으로 그린다. 표를 요청하면 표를 보여준다. 표를 설명하는 문단이 아니다.
Generative UI 는 에이전트가 설명 대신 직접 보여주도록 하는 레이어다. 이를 구축하는 세 가지 패턴이 등장했으며, 그 차이는 대부분의 팀이 생각하는 것보다 훨씬 중요하다.
하지만 이를 구축하는 방법은 하나가 아니다. 세 가지다. 그리고 대부분의 팀은 자신이 선택했다는 사실조차 모른 채 하나를 고른다.
프로토콜 스택
세 가지 프로토콜. 각각 하나의 역할을 한다.
MCP 는 에이전트를 도구에 연결한다. A2A 는 에이전트를 서로 연결한다. AG-UI 는 에이전트를 사용자에게 연결한다.
AG-UI 는 아래에서 볼 모든 것(도구 호출, A2UI 스키마, MCP App 이벤트, 상태 델타)을 전달하는 스트리밍 레이어다. SSE 를 통해 실행된다. 상태는 동일한 스트림에서 양방향으로 흐른다. 사용자가 편집하면 에이전트가 확인한다. 에이전트가 변경하면 사용자가 확인한다.
A2UI 는 에이전트가 UI 를 스키마로 내보내기 위한 Google 의 사양이다. AG-UI 위에서 작동한다. CopilotKit 이 프로덕션에 제공한다.
이를 위한 파서를 직접 작성할 필요는 없다. CopilotKit 은 AG-UI 클라이언트이며, 사용자를 대신해 스트림을 디코딩한다.
대부분의 팀이 혼동하는 세 가지 패턴
열 명의 개발자에게 Generative UI 가 무엇인지 물어보면 열 가지 답을 얻을 수 있다. 대부분은 현재 자신의 프레임워크가 제공하는 패턴을 설명하고 있을 뿐이다.
실제로는 세 가지뿐이다. 스펙트럼은 더 많은 제어에서 더 많은 유연성으로 이어진다.
- 제어형(Controlled): 컴포넌트를 미리 빌드한다. 에이전트는 렌더링할 것을 선택한다.
- 선언형(Declarative): 에이전트가 스키마를 내보낸다. 앱이 이를 컴포넌트에 매핑한다.
- 개방형(Open-ended): 에이전트가 원시 HTML 을 작성한다. 앱이 이를 샌드박스에서 렌더링한다.

2026 년의 모든 Gen UI 프레임워크는 이 선상 어딘가에 위치한다. 차이는 구조적이지 미용적이 아니다. 각 패턴은 규모가 커질 때 앱을 각기 다른 방식으로 망가뜨린다.
여러 스택을 시도해봤다. 대부분은 한 가지 패턴을 잘 지원한다. AG-UI 를 기반으로 동일한 런타임에서 세 가지를 모두 지원하는 CopilotKit 에 정착했다. 아래의 모든 내용은 이 스택에서 실행된다.
패턴 1: 제어형, 프론트엔드가 UI 를 소유

대부분의 팀이 여기서 시작한다. 또한 대부분의 팀이 여기서 막히기도 한다.
React 컴포넌트를 미리 빌드한다. 도구 이름에 바인딩한다. 에이전트가 해당 도구를 선택하면 컴포넌트가 에이전트의 인자를 props 로 사용하여 채팅에 인라인으로 렌더링된다.
프론트엔드 훅 하나면 된다. 에이전트 코드는 전혀 필요 없다. 그게 다다.
1"use client";2import { z } from "zod";3import { useComponent } from "@copilotkit/react-core/v2";45const expenseChartSchema = z.object({6 title: z.string(),7 data: z.array(z.object({ label: z.string(), value: z.number() })),8});910function ExpenseChart({ title, data }: z.infer<typeof expenseChartSchema>) {11 return (12 <section className="rounded-xl border p-4">13 <h3 className="text-sm font-medium">{title}</h3>14 <ul className="mt-2 grid gap-1">15 {data.map((d) => (16 <li key={d.label} className="flex justify-between text-sm">17 <span>{d.label}</span>18 <span>${d.value}</span>19 </li>20 ))}21 </ul>22 </section>23 );24}2526export function ExpensesCopilot() {27 useComponent({28 name: "showExpenseChart",29 description: "Render a breakdown of expenses by category.",30 parameters: expenseChartSchema,31 render: ExpenseChart,32 });3334 return null;35}
이 훅은 도구를 CopilotKit 의 런타임에 등록한다. 런타임은 AG-UI 를 통해 이를 에이전트에 광고한다. 에이전트가 호출하면 인자가 스트리밍되어 컴포넌트가 인라인으로 렌더링된다. 작성해야 할 Python 도구, 연결해야 할 스키마, 추가해야 할 API 경로가 없다.
디자인 시스템이 주도권을 쥔다.
해당 지출 차트는 목업이 아니다. AI 재정 코치 에이전트는 실제 예산, 저축 계획, 부채 상환을 위해 이와 같은 카드를 렌더링한다.
먼저 기본 훅을 원한다면? Generative UI Starter Project 의 'use-generative-ui-examples.tsx' 를 확인하라.
토큰 비용
등록된 모든 컴포넌트는 사용자가 아무 말도 하기 전에 에이전트의 컨텍스트 윈도우에 자리 잡는다. 일반적인 도구 설명과 해당 JSON 스키마는 약 400 토큰을 차지한다. 25 개의 컴포넌트는 매 턴마다 10,000 토큰이다. 요청마다 이 비용을 지불해야 한다.
에이전트가 잘못된 컴포넌트를 선택하기도 한다. 너무 많은 옵션이 비슷해 보이기 때문이다. 파이 차트와 도넛 차트 모두 "비율을 보여준다." 추측할 뿐이다.
에이전트 측 상태를 추가해야 하는 경우
공유 상태는 Python 도구를 작성할 가치가 있는 유일한 경우다. 에이전트가 세션 상태에 기록한다. UI 의 다른 부분이 이를 구독하고 두 번째 LLM 호출 없이 다시 렌더링된다. 지표를 고정하면 대시보드가 업데이트된다. 행을 추가하면 테이블이 다시 그려진다.
1from google.adk.agents import LlmAgent2from google.adk.tools import ToolContext34def pin_metric(tool_context: ToolContext, label: str, value: float) -> dict:5 """Pin a metric to the user's dashboard."""6 pinned = tool_context.state.get("pinnedMetrics", [])7 tool_context.state["pinnedMetrics"] = pinned + [{"label": label, "value": value}]8 return {"status": "pinned"}910agent = LlmAgent(name="dashboard_agent", model="gemini-3.5-flash", tools=[pin_metric])
프론트엔드는 CopilotKit 의 공유 상태 훅을 통해 고정된 지표를 읽는다. 동일한 도구 이름이 프론트엔드 훅으로 연결되어 있기 때문에 채팅 컴포넌트는 여전히 인라인으로 렌더링된다.
채팅에서 지표를 고정한다. 두 번째 모델 호출 없이 패널이 다시 그려진다. 이것이 바로 AI Dashboard Canvas Agent다.
AI 심층 조사 에이전트는 이를 한 단계 더 발전시킨다. 계획, 각 검색, 모든 파일 쓰기 등 모든 것이 라이브 카드로 스트리밍된다. 그 외의 모든 것에는 프론트엔드 훅만 있으면 된다.
제어형을 사용해야 하는 경우: 가치가 높은 열 개 이하의 플로우. 디자인의 정밀성이 중요하다. 필요한 정확한 UI 를 알고 있다.
사용하지 말아야 하는 경우: 유스케이스가 늘어남에 따라 코드베이스가 선형적으로 증가한다. 25 개의 컴포넌트는 모든 에이전트 턴에 25 개의 도구 정의가 있음을 의미한다.
무엇이 문제가 되는가: 에이전트가 잘못된 컴포넌트를 선택함. 두 도구 설명이 의미상 중복됨. 15 개 이상의 도구 중 두 개는 아마도 "데이터를 표시합니다"처럼 읽힐 것임. 해결책: 시각적 요소가 아닌 사용자 의도를 명명하도록 설명을 다시 작성하라. "전체의 비율을 비교하라는 사용자 요청 시 사용"이 "파이 차트를 렌더링합니다"보다 낫다.
패턴 2: 선언형 (A2UI), 에이전트가 스키마를 내보냄

이것은 대부분의 프로덕션 에이전트 앱이 결국 필요로 하는 패턴이다.
에이전트가 UI 를 설명하는 JSON 스키마를 내보낸다. 앱에는 스키마 노드를 React (또는 Svelte, Flutter 등 무엇이든)에 매핑하는 컴포넌트 카탈로그가 있다. 하나의 도구로 여러 UI 를 처리한다.
A2UI 가 표준 사양이다. CopilotKit 이 런타임을 제공한다. ADK 가 에이전트를 실행한다. AG-UI 가 연결 수단이다.
에이전트 도구는 순서대로 세 가지 작업을 반환한다: 표면 생성, 컴포넌트 트리 푸시, 데이터 푸시.
1def search_flights(flights: list[Flight]) -> dict[str, Any]:2 """Search flights and display them as rich cards."""3 return {4 "a2ui_operations": [5 {"type": "create_surface", "surfaceId": SURFACE_ID, "catalogId": CATALOG_ID},6 {"type": "update_components", "surfaceId": SURFACE_ID, "components": FLIGHT_SCHEMA},7 {"type": "update_data_model", "surfaceId": SURFACE_ID, "data": {"flights": flights}},8 ]9 }
이것은 실제 함수이며, 의사 코드가 아니다. 런타임 미들웨어는 도구 결과에서 a2ui_operations 컨테이너를 확인하고 표면을 프론트엔드로 전달한다. 호텔을 추가하는 경우? 새로운 스키마 파일. 다른 표면 ID 를 가진 함수 하나 더. 프론트엔드 작업은 전혀 필요 없다.
고정 스키마 vs 동적 스키마
위의 컴포넌트 트리는 flights.json 에 있다. 직접 작성한 것이다. 에이전트는 데이터만 채운다. 이것이 고정 스키마다.
동적 스키마는 그 반대다: 보조 LLM 이 대화 컨텍스트를 기반으로 턴마다 컴포넌트 트리를 작성한다. 마지막에 동일한 a2ui_operations 컨테이너가 온다. Google ADK 쇼케이스는 둘 다 제공한다.
카탈로그는 계약이다
정의 목록은 에이전트가 내보낼 수 있는 컴포넌트를 props 의 Zod 스키마와 함께 나열한다. 렌더러는 React 로 채운다. 오타는 빈 화면 대신 빌드 오류가 된다.
1const renderers: CatalogRenderers<TravelDefinitions> = {2 FlightCard: ({ props }) => (3 <article className="rounded-xl border p-4">4 <header className="flex justify-between">5 <span>{(props as any).airline}</span>6 <span>{(props as any).price}</span>7 </header>8 <div className="text-sm text-muted-foreground">9 {(props as any).origin} → {(props as any).destination} · {(props as any).departureTime}10 </div>11 </article>12 ),13};1415export const travelCatalog = createCatalog(travelDefinitions, renderers, {16 catalogId: "copilotkit://travel-catalog",17 includeBasicCatalog: true,18});
두 부분 모두 Generative UI Starter Project 에 연결되어 있고 일치한다. 'a2ui_fixed_schema.py' 의 search_flights 와 'renderers.tsx' 의 FlightCard 카탈로그. 항공편을 요청하면 카드가 채팅으로 스트리밍되는 것을 볼 수 있다.
버튼 및 기타 대화형 컴포넌트는 스키마에 액션을 전달한다. 기본 카탈로그는 이를 onClick 에 연결한다. 클릭하면 AG-UI 를 통해 이벤트가 에이전트로 다시 전송된다. 에이전트가 다음에 렌더링할 것을 결정한다. 클릭 핸들러는 전혀 필요 없다.
토큰 계산
카드 유형이 50 개든 500 개든, 에이전트는 하나의 함수만 본다. 컴포넌트 라이브러리가 커져도 턴당 토큰 수는 일정하게 유지된다.
JSON 이기 때문에 모든 렌더링 프레임워크로 확장 가능하다. 이미 AG-UI 를 사용하는 모든 에이전트는 첫날부터 A2UI 를 구동할 수 있다. 이를 연결하기 위해 에이전트 코드를 건드릴 필요가 없다.
트레이드오프: LLM 이 레이아웃을 소유한다. 출력은 카탈로그 내에서 실행될 때마다 달라진다. 법적 고지, 마케팅 표면, 또는 정확한 픽셀 배치가 중요한 모든 것이라면 이 패턴은 적합하지 않다.
선언형은 롱테일을 위해 만들어진 패턴이다. 대시보드, 결과, 양식, 카드, 위젯.
선언형을 사용해야 하는 경우: 미리 구축할 시간보다 더 많은 유스케이스가 있음. 프로토타입 단계를 넘어 토큰 경제에 신경 씀.
무엇이 문제가 되는가: 맞춤 FlightCard 를 구축했지만 모든 항공편이 기본 카탈로그의 일반 카드로 렌더링됨. 콘솔에 오류 없음. 에이전트의 CATALOG_ID 와 프론트엔드 createCatalog 의 catalogId 가 일치하지 않음. 프론트엔드가 에이전트가 대상으로 하는 카탈로그를 인식하지 못하고 기본으로 대체됨. 양쪽에서 문자열을 정확히 일치시켜라.
패턴 3: 개방형, 카탈로그도 규칙도 없음

세 번째 패턴은 정반대의 극단이다. 카탈로그도, 스키마도 없다. 그저 빈 캔버스일 뿐이다.
이 버킷에는 두 가지 하위 패턴이 있다.
MCP Apps
MCP 서버는 에이전트가 구동하는 UI 표면을 노출한다. Excalidraw 가 제게 가장 인상 깊었던 예시다. 에이전트가 캔버스를 완전히 제어한다. 컨텍스트에서 다이어그램을 그리고 보드의 모든 픽셀을 소유한다.

클라이언트 프로토콜을 처음부터 구현하는 것은 고통스럽기 때문에, CopilotKit 은 MCPAppsMiddleware 를 제공한다. 에이전트에 연결하고 모든 MCP Apps 서버를 가리키면 된다.
1const agent = new BuiltInAgent({2 model: "openai/gpt-5.5",3 prompt: "You are a helpful assistant.",4}).use(5 new MCPAppsMiddleware({6 mcpServers: [{ type: "http", url: "https://mcp.excalidraw.com/mcp", serverId: "my-server" }],7 }),8);
MCP Apps Showcase 를 실행하면 채팅 창 안에서 항공편을 예약하고 호텔을 예약할 수 있다. 동일한 미들웨어, 실제 MCP 서버. 또는 더 나아갈 수도 있다.
AI MCP App Builder는 에이전트가 E2B 샌드박스에 완전히 새로운 앱을 작성한 다음 실시간으로 렌더링하도록 한다.
0:45
샌드박스 HTML
에이전트가 원시 HTML 을 작성한다. 앱이 세션을 하이재킹할 수 없도록 샌드박스 처리된 iframe 내에서 렌더링한다.
런타임은 HTML 렌더링 도구를 등록하고 AG-UI 를 통해 에이전트에 전송한다. 에이전트는 원하는 마크업으로 이를 호출한다. 에이전트 측에서 정의할 HTML 도구는 없다. 런타임이 주입한다.
에이전트 측 지시사항이 실제 작업을 수행한다:
1canvas_agent = LlmAgent(2 name="canvas_agent",3 model="gemini-3.5-flash",4 instruction=(5 "You are a visualization assistant. When the user asks to see, "6 "draw, or visualize anything, generate an interactive HTML UI. "7 "Use Tailwind classes only. No external fonts. Stick to neutral "8 "colors unless the user names one."9 ),10)
이러한 스타일 규칙이 없으면 모델은 해당 주에 훈련 데이터에서 가장 두드러졌던 미적 감각을 기본값으로 사용한다. 규칙을 적용하면 대부분의 경우 브랜드에 가까운 결과를 얻을 수 있다. 항상 그런 것은 아니다.
브랜드 일관성 문제
에이전트의 기본 UI 로 개방형을 사용해 보려고 시도했다. 일주일 만에 철회했다.
화요일은 "네오-브루탈리스트", 수요일은 "iOS 4 클론". 프롬프트의 스타일 규칙은 에이전트를 브랜드 쪽으로 유도하지만 보장하지는 않는다. 브랜드가 계속 바뀌었고 제품이 진지하지 않게 느껴졌다.

개방형이 쓸모없다는 것은 아니다. 잘못 적용된 것이다.
한 가지에 올바른 선택이다: 사용자가 인터페이스의 모양을 신경 쓰지 않고 다시 볼 일이 없는 일회성 상호작용. "전자가 어떻게 작동하는지 보여줘." "지난 10 개의 쿼리에 대한 이상한 막대 차트를 줘." "이 API 응답을 시각화해줘." Google AI 개요에서 볼 수 있는 종류의 것들이다.
개방형을 사용해야 하는 경우: 일회성 쿼리. 일회용 시각화. 샌드박스 처리된 실험. 절대 기본 표면으로는 사용하지 말 것.
무엇이 문제가 되는가: iframe 은 렌더링되지만 버튼이 클릭되지 않음. 양식이 제출되지 않음. 샌드박스 플래그가 너무 엄격하거나 너무 느슨하여 브라우저가 거부함. iframe 샌드박스를 allow-scripts 와 allow-forms 로만 설정하라. 그 외에는 아무것도 허용하지 말라. 절대 allow-same-origin 을 허용하지 말라.
선택 방법
코드를 작성하기 전에 의사 결정 트리를 실행하라.
디자이너가 이 플로우에 대한 픽셀 완벽한 목업을 가지고 있는가? 제어형.
배송해야 할 카드 유형이나 위젯이 수십 개인가? 선언형.
사용자가 두 번 다시 보지 않을 일회성, 일회용 시각화인가? 개방형.
결정할 수 없나? 기본값은 선언형으로. 상위 3 개 플로우는 제어형으로 업그레이드. 절대 개방형을 기본값으로 사용하지 말 것.
이미 배송 중이고 현재 위치를 모르겠다면 렌더 도구의 수를 세어보라. 15 개를 넘었다면 제어형에 있으며 한계에 가까워지고 있다. 이번 주에 A2UI 연결을 시작하라.
세 가지 패턴. 세 가지 베팅.
제어형은 당신에게 건다. 사전 구축된 컴포넌트, 픽셀 완벽. 25 개를 넘으면 비용이 많이 든다.
선언형은 스키마에 건다. 스키마가 계약이다. 에이전트가 채운다. 수평으로 확장된다.
개방형은 모델에 건다. 카탈로그도, 스키마도 없고, 원시 HTML. 일회용에는 좋지만 두 번 이상 배송되는 모든 것에는 취약하다.
실수는 잘못된 패턴을 선택하는 것이 아니다. 자신이 하나를 선택했다는 사실을 모르는 것이다.
대부분의 팀은 프레임워크가 기본값으로 제어형을 제공하기 때문에 제어형을 기본값으로 사용한다. 25 개의 컴포넌트에서 한계에 부딪히고 데모에서 매력적으로 보이기 때문에 개방형을 선택한다. 둘 다 결정이 아니었다. 둘 다 표류였다.
의도적으로 선택하라. 문제에 패턴을 맞춰라. 정확해야 하는 플로우에는 제어형을. 롱테일에는 선언형을. 일회용에는 개방형을.
오픈 소스 Generative UI 에이전트 템플릿
세 가지 모두에 대한 참조는 awesome-llm-apps 의 새로운 Generative UI Agents 섹션에 있다. 필요한 것을 복제하고 필요 없는 것은 제거하라.
프로덕션에서 에이전트 배송, AG-UI, 그리고 확장되는 패턴에 대해 더 많은 내용을 게시할 예정이다. 계속 지켜보려면 @Saboo_Shubham_ 를 팔로우하라.










