Generative UI를 실제로 구현하는 방법 (LLM이 React 코드를 작성하도록 두지 않고)
요약
Generative UI를 구현하는 실용적인 방법으로, LLM이 직접 React 코드를 생성하게 하는 대신 구조화된 설명(JSON)을 통해 필요한 컴포넌트의 종류와 속성을 모델이 결정하도록 합니다. 애플리케이션은 이 설명을 받아 사전에 정의된 신뢰할 수 있는 컴포넌트 레지스트리에 매핑하여 렌더링합니다. 이를 통해 유연성과 안정성이라는 두 마리 토끼를 잡을 수 있습니다.
핵심 포인트
- LLM이 직접 코드를 생성하는 대신, 구조화된 JSON으로 UI 구성을 요청한다.
- 애플리케이션은 사전에 정의된 컴포넌트 레지스트리를 사용하여 렌더링을 제어한다.
- 모델은 기존 컴포넌트를 조합하여 동적이고 새로운 레이아웃을 구성할 수 있다.
- 이 방식은 테스트 용이성, 보안, 일관성 등 안정성을 확보하는 데 유리하다.
처음 Generative UI에 대해 들었을 때, 제 이해는 LLM에게 프롬프트를 제공하면 그것이 즉석에서 React 컴포넌트를 생성한다는 것이었습니다.
그것은 저를 Generative UI에 흥미를 느끼게 했습니다. 왜냐하면 이를 통해 사용자 개개인에게 항상 동일한 UI를 보여주는 방식에서 벗어날 수 있는 방법을 얻기 때문입니다. 비록 그 UI가 사용자가 하려는 일에 가장 적합하지 않을 수도 있더라도 말이죠.
Generative UI를 사용하면, 같은 데이터라도 사용자의 의도에 따라 다르게 제시될 수 있습니다. 한 사용자는 간단한 텍스트 답변이 필요할 수 있는 반면, 다른 사용자는 동일한 데이터를 차트(chart), 표(table) 또는 인터랙티브 인터페이스로 이해하는 것이 더 좋을 수도 있습니다.
하지만 LLM이 React 컴포넌트를 직접 즉석에서 생성하도록 내버려 두는 것에 대해 생각할수록, 저는 더 많은 문제점들을 발견했습니다.
테스트가 어려워집니다. 왜냐하면 모델이 항상 동일한 구조나 형식을 생성한다고 기대할 수 없기 때문입니다. 또한 보안(security), 접근성(accessibility), 테마 지정(theming), 신뢰성(reliability) 그리고 애플리케이션의 디자인 시스템과의 일관성에 대한 우려도 있습니다.
생성된 컴포넌트가 기술적으로 작동하더라도, 제품이 기대하는 방식으로 동작하지 않을 수 있습니다.
Generative UI를 구현하는 실용적인 방법 중 하나는 모델이 임의의 React 컴포넌트를 직접 생성하도록 하는 대신, 애플리케이션에 이미 존재하는 신뢰할 수 있는(trusted) 컴포넌트들을 사용하도록 하는 것입니다.
모델이 UI를 구성하게 하고, 구현은 하지 않도록 하기
이미 다음과 같은 컴포넌트들이 존재하는 애플리케이션을 상상해 보세요:
Card
BarChart
LineChart
...
LLM에게 JSX 작성을 요청하는 대신, 모델이 자신이 표시하고 싶은 내용을 구조화된 설명으로 생성할 수 있습니다.
예를 들어:
{
"type": "bar-chart",
"props": {
...
그러면 애플리케이션은 그 응답을 신뢰할 수 있는 React 컴포넌트에 매핑할 수 있습니다.
const componentRegistry = {
"bar-chart": BarChart,
table: Table,
...
작은 렌더러(renderer)는 다음과 같을 수 있습니다:
function GenerativeUI({ element }: { element: UIElement }) {
const Component = componentRegistry[element.type];
if (!Component) {
...
모델이 어떤 종류의 UI가 유용한지 결정합니다.
하지만 그 컴포넌트가 어떻게 구현될지는 여전히 애플리케이션이 제어합니다.
저에게는 그것이 훨씬 더 안전한 경계입니다.
모델은 인터페이스가 무엇을 필요로 하는지 결정할 수 있지만, 애플리케이션은 여전히 어떤 빌딩 블록을 사용할 수 있는지 제어합니다.
기존 컴포넌트를 사용한다고 해서 UI가 고정되어야 하는 것은 아니다
이러한 접근 방식의 한 가지 우려는 너무 제한적으로 들릴 수 있다는 것입니다.
만약 모델이 기존 컴포넌트만을 사용할 수 있다면, 우리가 정말로 UI를 생성하고 있는 걸까요?
저는 그렇다고 생각합니다.
모델은 미리 만들어진 단일 화면을 선택할 필요가 없습니다. 기존 컴포넌트들을 조합하여 새로운 구성을 만들 수 있습니다.
예를 들어:
Card
├── 지출 요약 (Spending summary)
├── 막대 차트 (Bar chart)
...
또는:
Stack
├── 요약 카드 (SummaryCard)
├── 카테고리 세부 내역 (CategoryBreakdown)
...
레이아웃 자체는 여전히 동적일 수 있습니다.
모델은 한 요청에는 차트가 필요하고 다른 요청에는 표가 필요하다고 결정할 수 있습니다. 또한 더 복잡한 답변을 위해 여러 컴포넌트를 결합할 수도 있습니다.
제 관점에서는, 모델이 완전히 새로운 차트 구현을 발명하거나 임의의 JavaScript를 생성하여 애플리케이션에 직접 실행하는 것은 바람직하지 않습니다.
그것은 UI 시스템에 대한 제어권을 포기하지 않으면서 모델에게 유연성을 부여합니다.
모든 응답이 차트가 될 필요는 없다
생성형 UI(Generative UI)라는 것이 모든 AI 응답을 시각적 인터페이스로 전환한다는 의미여서는 안 됩니다.
사용자가 다음과 같이 질문했다고 가정해 봅시다:
이번 달에 가장 큰 거래는 무엇이었나요?
만약 답변이 단순히 다음과 같다면:
가장 큰 거래는 Example Airlines에서 1,240달러였습니다.
그렇다면 일반 텍스트로 충분할 가능성이 높습니다.
차트는 가치를 크게 더하지 않으면서 복잡성만 추가할 것입니다.
하지만 사용자가 다음과 같이 질문한다면:
이번 달에 어떤 카테고리에 가장 많이 지출했나요?
그렇다면 시각적 비교가 더 유용해집니다.
막대 차트는 단락보다 답변을 더 빠르게 전달할 수 있습니다.
따라서 목표는 다음과 같아서는 안 됩니다:
AI 응답
↓
시각적 UI 생성
오히려 다음과 가깝습니다:
사용자 의도 이해
↓
유용한 표현 선택
...
최적의 UI는 사용자가 무엇을 이해하거나 하려고 하는지에 따라 달라집니다.
생성하기 전에 명확히 하기
이 질문을 고려해 보세요:
이번 달에 가장 많은 돈을 어디에 썼나요?
그 질문은 모호합니다.
사용자가 다음 중 어떤 의미인지 묻는 것일까요?
- 카테고리별로?
- 가맹점별로?
- 가장 큰 단일 거래액으로?
모델은 가정(assumption)을 세워 다듬어진 차트를 생성할 수 있습니다.
하지만 그 가정이 틀릴 경우, UI는 잘못된 질문에 답하면서도 유용해 보일 수 있습니다.
저는 먼저 더 많은 맥락을 요청하는 것이 낫다고 생각합니다.
예를 들어:
사용자님은 다음 중 어떤 것을 의미하나요?
• 카테고리별 지출
• 가맹점별 지출
...
사용자가 명확히 하면, 시스템은 더 나은 표현을 선택할 충분한 정보를 갖게 됩니다.
이것은 저에게 있어 생성형 UI(Generative UI)의 중요한 부분입니다.
대화(conversation)를 대체해서는 안 됩니다.
더 좋은 UI 결정을 내리기 위해 대화를 사용해야 합니다.
모델은 어떻게 표현을 선택해야 할까요?
여기서도 LLM이 무제한적인 자유를 가져서는 안 된다고 생각합니다.
실제 상용 애플리케이션(production application)은 규칙과 허용되는 패턴을 정의할 수 있습니다.
예를 들어:
단일 사실(Single fact)
↓
텍스트
...
모델은 이러한 규칙들을 사용자의 의도와 함께 사용하여 가장 유용한 표현을 선택할 수 있습니다.
예를 들어:
"제 지출 카테고리를 비교해 주세요"
↓
막대 차트(Bar chart)
...
이것은 모델에게 어느 정도의 자유를 주면서도 출력을 예측 가능한 애플리케이션 경계 내에 유지하게 합니다.
전체 디자인 시스템을 노출하지 마세요
실제 애플리케이션에는 수백 개의 컴포넌트가 있을 수 있습니다.
저는 모델이 이 모든 것에 접근할 필요는 없다고 생각합니다.
대신, AI 생성 경험에 유용한 선별된(curated) 컴포넌트 세트를 노출하는 것이 좋겠습니다.
예를 들어:
카드(Card)
테이블(Table)
막대 차트(BarChart)
...
금융 애플리케이션은 도메인별 컴포넌트도 노출할 수 있습니다:
거래 내역 테이블(TransactionTable)
지출 요약(SpendingSummary)
카테고리 분류(CategoryBreakdown)
...
식단 계획 애플리케이션은 다음을 노출할 수 있습니다:
식단 계획(MealPlan)
레시피 카드(RecipeCard)
쇼핑 목록(ShoppingList)
...
더 작은 카탈로그는 시스템이 추론하고 검증하기 쉽게 만듭니다.
또한 모델이 생성 경험에 사용되도록 의도되지 않은 컴포넌트를 선택할 가능성을 줄여줍니다.
모델은 유용한 인터페이스를 만들기에 충분한 빌딩 블록을 얻어야 하지만, 전체 내부 디자인 시스템을 가져서는 안 됩니다.
모델 출력을 신뢰할 수 없는 입력으로 취급해야 합니다
모델이 구조화된 UI 설명만을 생성하더라도, 이를 맹목적으로 렌더링해서는 안 됩니다.
출력은 여전히 검증되어야 합니다.
예를 들어:
const result = uiSchema.safeParse(modelOutput);
if (!result.success) {
return fallbackResponse;
...
만약 모델이 지원되지 않는 컴포넌트를 요청한다면, 애플리케이션은 안전하게 실패해야 합니다.
const Component = componentRegistry[element.type];
if (!Component) {
return <FallbackMessage />;
...```
폴백(fallback)은 일반 텍스트, 오류 메시지이거나 지원되는 컴포넌트를 사용하는 또 다른 시도일 수 있습니다.
중요한 점은 모델의 출력을 신뢰할 애플리케이션 코드가 아닌 데이터로 취급해야 한다는 것입니다.
이렇게 하면 애플리케이션 자체가 보안 경계(security boundary)를 유지하게 됩니다.
모델이 UI나 동작을 제안할 수는 있지만, 일반적인 애플리케이션 권한 및 검증은 여전히 적용되어야 합니다.
## 모든 사용자 동작에 모델 호출이 필요한 것은 아닙니다
생성된 UI가 다음과 같다고 가정해 봅시다:
Spending by Category
Dining $620
Travel $480
...
이 두 가지 동작은 인터페이스에서 비슷해 보이지만, 반드시 같은 방식으로 처리되어야 하는 것은 아닙니다.
만약 사용자가 다음을 클릭한다면:
**Dining 거래 내역 보기**
애플리케이션은 이미 무엇을 해야 할지 알고 있습니다.
현재 데이터를 필터링하거나 일반적인 API 호출을 수행할 수 있습니다.
function viewDiningTransactions() {
filterTransactions({
category: "Dining"
...```
이것을 LLM을 통해 다시 보낼 이유가 없습니다.
하지만 사용자가 다음을 클릭한다면:
Dining 지출 증가 설명 요청
이것은 해석(interpretation)을 필요로 합니다.
그것이 모델을 다시 개입시키기에 좋은 이유입니다.
function handleAction(action: Action) {
if (action.type === "view-transactions") {
filterTransactions(action.category);
...```
여기서 얻는 간단한 규칙은 다음과 같습니다:
AI가 UI를 생성했다는 이유만으로가 아니라, 지능이 필요할 때 LLM을 사용해야 합니다.
결정론적인 동작(Deterministic actions)은 결정론적 상태로 유지되어야 합니다.
이렇게 하면 애플리케이션 테스트가 더 쉬워지고 불필요한 모델 호출이 일반 UI 상호작용에서 제외됩니다.
## 제가 시작할 아키텍처
이 모든 것을 종합하면, 제가 시작할 아키텍처는 다음과 같습니다:
User
↓
LLM / Agent
...
모델은 다음과 같은 내용을 반환할 수 있습니다:
{
"elements": [
{
...
애플리케이션이 해당 구조를 검증하고 지원되는 컴포넌트만 렌더링합니다.
이는 여전히 제게 Generative UI처럼 느껴집니다.
모델은 다음에 어떤 경험이 나타나야 할지 결정하지만, 프론트엔드 아키텍처를 우회하는 것은 아닙니다.
## 어려운 부분은 React를 렌더링하는 것이 아니다
실제 React 렌더링 자체는 아마도 비교적 쉬운 부분일 것입니다.
매핑:
{
"type": "bar-chart"
}
을:
으로 매핑하는 것은 간단합니다.
더 어려운 질문들은 경계(boundary)에 관한 것입니다.
만약 모델이 유효하지 않은 컴포넌트를 요청하면 어떻게 할까요?
만약 props가 스키마와 일치하지 않으면요?
만약 사용자가 특정 작업을 수행할 권한이 없다면요?
선택된 시각화는 유효하지만 오해의 소지가 있다면요?
다양하게 생성되는 조합들을 어떻게 테스트할까요?
접근성(accessibility)을 어떻게 보장할까요?
컴포넌트가 진화함에 따라 UI 스키마를 어떻게 버전 관리할까요?
모델에게 어떤 컴포넌트들이 사용 가능해야 할지 어떻게 결정할까요?
이것들이 바로 Generative UI를 저에게 흥미롭게 만드는 부분들입니다.
단순히 LLM이 React를 생성하는 것이 아닙니다.
이는 확률적 모델과 결정론적 프론트엔드 시스템 간의 새로운 상호작용입니다.
제 생각이 도달한 지점
Generative UI에 대한 저의 초기 이해는 간단했습니다:
LLM에게 프롬프트를 주고 즉석에서 React 컴포넌트를 생성하도록 하는 것입니다.
저는 여전히 인터페이스를 사용자에게 동적으로 적응시키는 아이디어를 좋아하지만, 무제한적인 컴포넌트 생성이 올바른 프로덕션 경계라고 생각하지 않습니다.
차라리 모델이 사용자가 어떤 종류의 경험을 필요로 하는지 결정하게 하고, 애플리케이션이 실제 컴포넌트, 검증(validation), 권한(permissions), 접근성(accessibility) 및 실행을 제어하도록 할 것입니다.
모델은 프론트엔드를 대체하는 것이 아닙니다.
사용자가 다음에 어떤 프론트엔드 경험이 필요한지 결정하는 것을 돕는 것입니다.
그리고 저에게는 단순히 LLM에게 React를 작성하도록 요청하는 것보다 훨씬 더 흥미로운 일입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Hacker Noon AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기