---
type: note
kind: Recipe
title: GenUI 도입 — 모델이 정하는 게 인자인가 배치인가 코드인가
description: Static/Declarative/Open-ended 판별부터 카탈로그 설계·신뢰 경계·회귀 전략까지.
tags: ['genui', 'llm', 'react', 'design-system', 'security']
status: release
ctime: 2026-09-01
mtime: 2026-09-01
generated: { by: claude/opus-5, at: 2026-09-01T00:00:00Z }
sources:
  - id: boda-generative-ui
    title: "boda.sh — Generative UI (2026-07-29 발표 슬라이드)"
---

**언제** — 누군가 "우리도 GenUI 해보자"고 말했을 때. 회의에서 그 단어가 나오면 **셋 중 뭔지부터 확정한다.**

| 이름 | 모델이 정하는 것 | 실행되는 코드의 출처 | 검증 표면 |
|---|---|---|---|
| **Static/Controlled** | 툴 인자 | 우리 코드 | 툴 스키마 |
| **Declarative** | 컴포넌트 트리·배치·props | 우리 코드 | 카탈로그(Zod) |
| **Open-ended** | 코드 전체 | 모델 출력 | 없음 (샌드박스만) |

직교하는 두 번째 축은 **transport** — 우리 앱에 그리나 남의 호스트에 그리나. "어디에 그리나"와 "얼마나 자유롭나"는 별개 결정이고 섞을 수 있다.

---

## R1. 지금 GenUI 가 필요한지 판별

1. 사용자 입력이 **열거 가능한 선택지**인가 자유 텍스트인가?
   - 열거 가능 → **여기서 중단.** 필터 UI 가 답이다.
2. 자유 텍스트일 때 **나올 수 있는 화면 종류가 몇 개**인가?
   - 5개 이하로 셀 수 있다 → **Static/Controlled. 대부분 여기서 끝난다.**
3. 구성이 달라지는 게 **데이터 양** 때문인가 **관심사 조합** 때문인가?
   - 데이터 양(0건/1건/N건) → Static 으로 충분. 컴포넌트 3개로 커버된다
   - 관심사 조합(어떤 요청은 날씨+일정, 어떤 요청은 예산+체크리스트) → **Declarative**
4. 우리 부품으로 표현이 아예 불가능한 일회성 위젯인가? → 그 조각만 **Open-ended**. 페이지 전체는 아님.

**함정** — "AI 붙이자"가 실제로는 "검색이 안 좋다"인 경우가 많다. 랭킹 문제를 GenUI 로 덮으면 비용만 늘고 문제는 남는다. 그리고 **데모에서 인상적이었던 건 대부분 Declarative 인데 실제 필요한 건 대부분 Static 이다.**

---

## R2. 인텐트 계약 (GenUI 도입 여부와 무관하게 먼저)

**언제** — 기획 → 디자인 → 구현 사이에서 케이스가 자꾸 누락될 때.

1. 기능 하나를 **인텐트 문장** 하나로 쓴다. "사용자가 조건을 말하면 조건에 맞는 장소를 찾아준다."
2. **입력 인자 집합**을 확정한다. 무엇이 필수고 무엇이 옵션인지.
3. **결과 상태 집합을 전부 열거한다. 이게 핵심이다.** 정상 경로가 아니라 나머지를 적는다.

```
- 결과 N건 / 결과 1건 (레이아웃이 다르면 별도 상태)
- 조건은 유효한데 결과 0건 / 조건 자체를 해석 못 함
- 부분 실패 (3개 소스 중 1개 죽음)
- 권한 없음 (데이터 없음과 반드시 구분)
- 오래된 캐시를 보여주는 중
```

4. 상태마다 **반환 필드**를 정한다. 최소한 "왜 이 결과가 나왔는지"를 UI 가 설명할 수 있어야 한다 — `{ query, appliedFilters, items, totalMatched }` 에서 `appliedFilters` 가 없으면 "왜 이게 나왔죠?"에 답할 수 없다.
5. 기획·디자인·프론트·서버가 **같이 승인한다.** 이게 계약이다.

**확인** — 디자인 시안 개수 == 결과 상태 개수인가? 적으면 누군가 런타임에 즉흥으로 정하게 된다. QA 가 이 목록만 보고 테스트 케이스를 쓸 수 있는가?

**왜 먼저 하나** — Static 으로 갈 때 이 정의가 **그대로 툴 스키마가 된다.** 순서가 뒤집히지 않는다. GenUI 를 끝내 안 하기로 해도 이 작업만으로 커뮤니케이션 비용이 준다.

---

## R3. Static/Controlled 도입 (기본값)

기존 컴포넌트를 쓴다 — **새로 안 만든다.**

1. 인텐트 계약을 툴 스키마로 옮긴다. 모델이 채울 수 있는 건 **입력 인자뿐**이다.
2. 툴 본체를 **앱 코드로** 쓴다. 필터·랭킹·권한 검사 전부 여기.
3. 반환값에 결과 상태를 판별할 필드를 넣는다.
4. 결과 상태 → 컴포넌트 매핑을 **결정적 코드로** 쓴다.

```ts
// 이 분기는 코드다. 모델이 정하지 않는다.
if (totalMatched === 0) return <EmptyMatch filters={appliedFilters} />
if (totalMatched === 1) return <PlaceCard place={places[0]} />
return <PlaceResults places={places} />
```

5. 모델에게는 **반환된 결과에 대한 설명만** 쓰게 하고, 결과 밖의 이야기를 못 하게 프롬프트로 못박는다.

**확인** — 툴 호출을 목으로 고정하면 UI 가 항상 같은 화면을 내는가?(아니면 결정성이 새고 있다) 모델을 완전히 죽여도 기존 필터 UI 로 기능이 동작하는가?(폴백 경로)

**함정** — 툴이 반환하지 않은 항목을 모델이 설명에 끼워 넣는다. 반환 목록을 프롬프트에 명시하고 후처리로도 검사한다. 그리고 **필터링을 모델에게 시키고 싶은 유혹** — 시키는 순간 재현성과 감사 로그가 동시에 사라진다.

---

## R4. Declarative 도입 (R1 의 3번일 때만)

여기부터 비용이 계단식으로 뛴다. 백엔드는 **그대로**고(에이전트는 안 바뀐다 — 이게 이 경로의 실용적 장점), 새로 만들어야 하는 유일한 것은 **컴포넌트 카탈로그**다.

```
에이전트  →  전송 스펙  →  UI 서술 스펙  →  React 컴포넌트
 (서버)   (어떻게 흐르나)  (모델이 뭘 생성)   (여전히 우리 코드)
```

1. 도메인 툴은 그대로 둔다.
2. 런타임이 렌더 툴을 주입하고 모델에 **카탈로그 스키마를 노출**한다. 모델이 뭘 쓸 수 있는지 아는 유일한 경로.
3. 순서를 강제한다 — **데이터 툴 먼저 → 렌더 서술은 마지막에 정확히 1회.**
4. 미들웨어가 인자를 스키마 검증 후 UI operation 으로 확장한다.
5. 클라이언트로 스트리밍. 로딩 상태와 완성 화면이 **같은 messageId** 를 쓰게 한다(append 아니라 replace).

**확인** — 카탈로그에 없는 컴포넌트를 요청했을 때 조용히 빠지는가, 에러인가, 폴백인가? 셋 중 하나를 **명시적으로** 고른다. 렌더 툴이 두 번 호출되면? 스트림이 중간에 끊기면?

**함정** — **정책이 타입 시스템이 아니라 프롬프트에 쌓인다.** 타입 검사도 테스트도 안 되는 규칙 더미가 자라는 게 이 경로의 진짜 비용이다. 조합 폭발도 사라진 게 아니라 **카탈로그 유지보수로 이동**했을 뿐이다. 그리고 데모가 죄다 read-only 대시보드인 건 우연이 아니다 — **상호작용·로컬 상태·폼 검증**이 이 방식에서 가장 약하다.

---

## R5. 카탈로그 설계 (R4 실제 작업량의 대부분)

카탈로그 = **Zod 정의(모델이 요청할 수 있는 것) + React 렌더러(실제 실행되는 것) + 버전 붙은 catalogId.**

1. **허용 목록이지 힌트가 아니다.** 목록 밖은 표현 자체가 불가능해야 한다.
2. **props 를 최대한 좁힌다.** `className` 같은 탈출구를 열면 카탈로그가 무의미해진다. 자유 문자열보다 열거형(`variant: "compact" | "full"`). `href`·`src` 는 **반드시 허용 도메인 검사** — 외부 이미지 하나가 비콘이 된다.
3. **레이아웃 컴포넌트를 명시적으로 넣는다.** 안 넣으면 모델이 텍스트로 레이아웃을 흉내 내기 시작한다.
4. **버전을 붙인다.** 카탈로그 변경은 API 변경과 같은 무게다.
5. **접근성은 렌더러가 소유한다.** 헤딩 레벨·포커스 순서·라벨을 모델에게 맡기면 요청마다 a11y 트리가 달라진다.

**소유권 질문이 "누가 UI 를 주도하는가"의 실제 답이다** — 서버 스키마를 계약으로 두면 백엔드가, 인텐트+결과 상태 집합이면 기획/도메인이, 컴포넌트 카탈로그면 디자인 시스템이 주도권을 갖는다. **카탈로그를 소유한 팀이 사실상 UI 결정권을 갖는다.** 암묵적으로 두지 말고 명시한다.

---

## R6. Open-ended 를 안전하게 한 조각만

1. 샌드박스 iframe. `allow-scripts` 만, **`allow-same-origin` 은 주지 않는다**(둘을 같이 주면 샌드박스가 무의미).
2. 부모와의 통신은 좁은 postMessage 채널 하나로만. origin 검사 필수.
3. 네트워크는 CSP 로 차단하거나 허용 목록만.
4. 페이지 안에서의 크기·스크롤 소유권을 고정한다.

**함정** — 디자인 시스템 충돌, 이중 스크롤바, 성능 비용, 그리고 **직접 관리해야 하는 보안 경계**가 생긴다. 지연·캐시 불가·감사 곤란이 전부 여기 몰려 있다. 프로덕션 기본값으로 쓰지 않는다.

---

## R7. 신뢰 경계 점검 (배포 전)

**"JSON 이라 안전하다"는 절반만 맞다.** 임의 코드 실행은 막지만 나머지는 안 막힌다.

1. **props 공격면** — `href`/`src` 가 카탈로그에 있고 도메인 검사가 있는가?
2. **프롬프트 인젝션이 유도한 정당해 보이는 UI** ← Declarative 의 실제 최대 위협. 검색 결과나 외부 문서 텍스트가 모델 컨텍스트로 들어오는가? 카탈로그에 폼·버튼·링크가 있으면 **모델이 만든 피싱 화면은 스키마 검증을 통과한다.** 대응은 외부 텍스트를 데이터로만 격리, 입력형 컴포넌트를 카탈로그에서 분리·화이트리스트, 렌더 결과 후처리 검사.
3. **권한** — 모델이 본 데이터 == 이 사용자가 볼 수 있는 데이터인가? 툴 레벨에서 걸렀는가?
4. **감사** — 어떤 요청이 어떤 UI 를 만들었는지 재현 가능한가?(모델 출력 + catalogId + 입력을 함께 로깅)

**코드 실행이 아니라 사회공학이 declarative 의 진짜 위협이고, 카탈로그는 이걸 막지 못한다.**

---

## R8. 테스트·회귀

UI 가 비결정적이 되는 순간부터 스냅샷/E2E 가 그냥 무너진다.

1. **층을 나눠서 테스트한다.** 툴 함수 → 순수 단위 테스트(모델 없음, 가장 많은 테스트가 여기). 컴포넌트 → 고정 props. 매핑 로직 → 결과 상태별 스냅샷. 모델 → 여기만 비결정적.
2. **모델 출력을 픽스처로 고정한다.** 매 CI 마다 모델을 부르지 않는다.
3. **스키마 위반을 계약 테스트로.** 카탈로그 변경 시 기존 픽스처가 깨지는지 본다.
4. **모델 품질은 별도 평가 세트로.** 대표 쿼리 N개 → 기대 컴포넌트 집합, 통과율을 지표로. E2E 와 섞지 않는다.

**함정** — 모델이 낀 E2E 를 CI 에 넣으면 플레이키가 되고 결국 다들 무시한다.

---

## R9. 실패 모드 정의 (안 정하면 장애 때 즉흥으로 정한다)

| 상황 | 정해야 할 것 |
|---|---|
| 모델 타임아웃 | 기존 UI 폴백? 에러 화면? |
| 카탈로그에 없는 컴포넌트 요청 | 무시 / 전체 실패 / 플레이스홀더 |
| 스트리밍 중단 | 부분 화면 유지 vs 롤백 |
| 도메인 툴 3개 중 1개 실패 | 부분 렌더 + 실패 표시 (조용히 빼지 말 것) |
| 비용·쿼터 초과 | 비-AI 경로로 강등 |

**킬 스위치** — 모델 경로를 끄면 R2 의 결정적 UI 로 돌아가는 플래그. 이게 있으면 훨씬 과감하게 실험할 수 있다.

---

## R10. 회의에서 쓰는 질문 세트

1. AI 가 정하는 게 정확히 뭐예요? **인자? 배치? 코드?**
2. 나올 수 있는 화면 종류를 셀 수 있어요, 없어요?
3. 카탈로그는 누가 소유해요?
4. "이 케이스는 왜 이렇게 나왔죠"라고 물으면 어디를 보고 답해요?
5. 모델을 끄면 이 기능은 어떻게 돼요?

**1번만 확정돼도 나머지 논의가 전부 명확해진다.**

---

**기억해둘 반론** — "스펙이 코드를 대체한다"는 절반만 맞다. 스펙과 SDK 도 코드다. 실제로 일어난 일은 추상화 상승이 아니라 **조합 결정 시점이 컴파일 타임에서 런타임으로 이동**한 것이고, 그 대가로 타입 안정성을 프롬프트 정책과 맞바꾼 것이다. 남은 열린 문제(지연·캐시·감사·일관성·비용, 상호작용 이후의 상태 왕복, 접근성 보장, 비결정적 UI 의 회귀 전략)는 전부 이 이동의 청구서다.
