# 생성형 UI

> AI 모델이 dsui 컴포넌트로 화면을 만들게 하는 방법이에요. OpenUI와 json-render를 지원해요.

- URL: https://dsui.vercel.app/docs/genui

## 생성형 UI가 뭐예요

생성형 UI는 AI 모델이 글이 아니라 화면을 답으로 내놓는 방식이에요. 모델은 미리 정해 둔 컴포넌트 목록 안에서만 화면을 쓰고, 앱은 그 답을 실제 컴포넌트로 그려요. dsui를 쓰면 모델이 만든 화면도 사람이 만든 화면과 같은 토큰, 같은 모션, 같은 효과음을 가져요.

dsui는 컴포넌트를 한 번만 정의하면 두 프레임워크에서 함께 써요.

| 프레임워크 | 모델이 쓰는 것 | dsui가 만들어 주는 것 |
| --- | --- | --- |
| OpenUI | OpenUI Lang(짧은 함수 호출 문법) | `defineComponent`와 `createLibrary`로 만든 라이브러리, 시스템 프롬프트 |
| json-render | JSON 스펙 | `defineCatalog`와 `defineRegistry`로 만든 카탈로그와 레지스트리, 시스템 프롬프트 |

## 컴포넌트를 한 번 정의해요

중립 스펙에는 이름, 모델이 언제 골라야 하는지(`description`), 속성(zod), 자식을 받는지(`slot`), 낼 수 있는 이벤트(`events`)를 적어요. 속성은 문자열, 숫자, 불리언, 열거형처럼 직렬화되는 것만 써요. 모델은 콜백이나 노드를 쓸 수 없어서, 동작은 `emit`(이벤트)과 `setValue`(입력 값)로 앱에 전달돼요.

```tsx
import { z } from "zod";
import { defineGenUi } from "@/components/arc/gen-ui/spec";
import { Button } from "@/components/arc/button/button";

export const GenButton = defineGenUi({
  name: "Button",
  description: "A button that triggers an action. Use a verb phrase as the label.",
  props: z.object({
    label: z.string(),
    variant: z.enum(["primary", "secondary", "ghost"]).optional(),
  }),
  events: ["press"],
  render: ({ props, emit }) => (
    <Button variant={props.variant} onClick={() => emit("press")}>
      {props.label}
    </Button>
  ),
});
```

OpenUI Lang는 인자를 위치로 넘기기 때문에, 속성은 필수 속성을 앞에 두고 선택 속성을 뒤에 둬요. 자식을 받는 컴포넌트는 자식 목록이 맨 앞이에요(`Card([...], "제목")`).

## OpenUI에서 쓰기

스펙을 라이브러리로 바꾸면 모델에게 줄 시스템 프롬프트와 화면을 그릴 `Renderer`가 함께 나와요. 이벤트는 `action` 속성(이벤트가 여럿이면 `on<이벤트>`)이 되고, 입력 값은 OpenUI의 폼 상태에 쌓여요. 자식을 받는 컴포넌트는 `children` 배열을 받아요.

```tsx
// Server (a route handler): the system prompt for the model.
import { GENUI_RULES, GENUI_SPEC } from "@/components/arc/gen-ui/components";
import { openUiPrompt } from "@/components/arc/gen-ui/openui";

const system = openUiPrompt(GENUI_SPEC, { rules: GENUI_RULES });

// Client: draw what the model wrote.
import { GenUi } from "@/components/arc/gen-ui/gen-ui";

<GenUi framework="openui" response={text} />;
```

## json-render에서 쓰기

같은 스펙에서 카탈로그와 레지스트리가 나와요. 선택 속성은 모델이 `null`로 보내도 받고, dsui 컴포넌트에는 빠진 속성으로 전달돼요. 자식은 `default` 슬롯으로 들어가고, 입력 값은 상태의 `/form/<이름>`에 쌓여요.

```tsx
// Server (a route handler): the system prompt for the model.
import { GENUI_RULES, GENUI_SPEC } from "@/components/arc/gen-ui/components";
import { jsonRenderPrompt } from "@/components/arc/gen-ui/json-render";

const system = jsonRenderPrompt(GENUI_SPEC, { rules: GENUI_RULES });

// Client: draw the spec the model wrote.
import { GenUi } from "@/components/arc/gen-ui/gen-ui";

<GenUi framework="json-render" response={spec} />;
```

## 설치하고, 지금 되는 것과 아직 안 되는 것

[`gen-ui`](/items/gen-ui)는 다른 컴포넌트처럼 한 줄로 설치해요. 쓰는 컴포넌트(Button, Input, Textarea, Select, Switch, Checkbox, Alert, Badge, MetricCard, Progress, Stack, Card)와 필요한 npm 패키지(`@openuidev/react-lang`, `@json-render/react`, `zod`)가 함께 들어와요.

```bash
npx shadcn@latest add https://dsui.vercel.app/r/gen-ui.json
```

지금은 미리 보기 단계라 열두 컴포넌트가 들어 있어요. 입력 필드(Input, Textarea, Select, Switch, Checkbox)는 값을 `name` 이름으로 앱에 알려 줘요.

- 자식을 받는 컴포넌트는 스펙에서 자기보다 앞에 적힌 컴포넌트만 품어요. 그래서 Card는 Stack(가로 한 줄 배치)을 품을 수 있지만, Stack 안에 Stack이나 Card 안에 Card는 없어요.
- 해요체와 버튼 문구 같은 dsui 규칙은 `GENUI_RULES`로 두 프레임워크의 프롬프트에 같은 문장으로 들어가요.
- 나머지 컴포넌트는 같은 방식으로 하나씩 더해 가요. 새 컴포넌트는 스펙만 쓰면 두 어댑터가 그대로 받아요.
