AI가 버튼을 움직일 때: 적응형 UI를 위한 지원 루프 구축하기
요약
AI가 생성하는 적응형 UI의 운영 안전성을 확보하기 위해, 모델이 직접 코드를 생성하는 대신 검증 가능한 매니페스트를 제안하도록 하는 제어 루프 구축 방법을 다룹니다. React와 TypeScript를 사용하여 UI 변경 사항을 버전 관리하고 인간이 제어할 수 있는 아키텍처를 제안합니다.
핵심 포인트
- AI에게 직접적인 코드 실행 권한 대신 구조화된 매니페스트 생성을 유도
- 승인된 컴포넌트와 액션만 표현 가능한 엄격한 UI 계약(Contract) 수립
- 생성된 UI의 식별, 재현 및 인간에 의한 고정(Freeze) 기능 필수
- 모델의 제어 범위를 UI 제안으로 한정하여 운영 안정성 확보
적응형 인터페이스(Adaptive interface)는 사용자가 "내보내기 버튼이 어디로 갔지?"라고 묻는 바로 그 순간 전까지는 매우 인상적으로 보일 수 있습니다.
그 질문은 개발자들에게 불편한 긴장감을 조성합니다. 모델이 그럴듯한 레이아웃을 생성할 능력은 있을지 모르지만, 그럴듯함(plausibility)이 운영상의 안전성(operational safety)과 동일한 것은 아닙니다. 지원 팀은 더 이상 존재하지 않는 화면을 조사할 수 없으며, 개발자는 오직 일시적인 UI의 스크린샷만이 유일한 증거일 때 시스템을 개선할 수 없습니다.
해답은 모든 픽셀을 보존하거나 모든 간격 변경에 대해 인간의 승인을 요구하는 것이 아닙니다. 생성된 UI를 세 가지 속성을 가진 버전 관리된 제안(versioned proposal)으로 취급하는 것입니다:
- 승인된 컴포넌트(components)와 액션(actions)만 표현할 수 있어야 합니다.
- 렌더링된 모든 버전은 식별 및 재현(replay)될 수 있어야 합니다.
- 인간은 모델에게 허가를 구하지 않고도 이를 고정(freeze)하거나 교체할 수 있어야 합니다.
이 튜토리얼에서는 React, TypeScript, Zod, 그리고 PostgreSQL을 사용하여 이러한 제어 루프(control loop)를 구축합니다.
하나의 사례로 보는 아키텍처
AI가 생성한 계정 화면이 눈에 띄는 구독 취소(Cancel subscription) 버튼을 모호한 플랜 관리(Manage plan) 메뉴로 대체한다고 가정해 봅시다.
운영 환경에 적합한 시퀀스는 다음과 같아야 합니다:
모델이 매니페스트(manifest)를 제안함
↓
스키마(Schema)와 정책(policy)이 이를 검증함
...
모델이 제어하지 않는 것에 주목하십시오: 영속성(persistence), 액션 권한(action permissions), 배포 상태(deployment status), 또는 롤백(rollback)입니다.
이 경계는 모델이 보여주는 능력—구조화된 인터페이스 제안을 생성하는 것—과 모델이 인터페이스를 안전하게 "소유"할 수 있다는 과장된 광고(hype)를 구분합니다. 어려운 부분은 여전히 인간의 영역으로 남아 있습니다: 어떤 변경이 무해한지, 어떤 보고서가 실제 피해를 나타내는지, 그리고 언제 새로움이 더 이상 불확실성을 감수할 가치가 없는지를 결정하는 일입니다.
1. 실행 가능한 UI 코드가 아닌 매니페스트를 생성하라
모델이 생성한 JSX, JavaScript, URL, 패키지 이름 또는 import 문을 평가하지 마십시오. 모델에게 이미 애플리케이션에 존재하는 액션을 가진 작은 UI 언어를 제공하십시오.
// ui-contract.ts
import { z } from "zod";
...
이 계약(contract)은 단순한 구문 오류(syntax error) 방지 이상의 역할을 합니다. 이는 공급망 실패 모드(supply-chain failure mode) 전체를 제거합니다. 종속성(dependencies)이 언어의 일부가 아니기 때문에, 생성된 레이아웃이 그럴듯해 보이지만 존재하지 않는 패키지를 도입할 수 없습니다.
패키지 변경 사항은 여전히 일반적인 리포지토리(repository), 락파일(lockfile), CI, 그리고 인간의 검토 프로세스를 거쳐야 합니다.
2. 생성된 구조 외부에 액션(actions) 유지하기
매니페스트(manifest) 내의 버튼은 애플리케이션의 기능(capability)을 명시할 뿐, 해당 기능을 정의하지는 않습니다.
// actions.ts
export const actions = {
open_profile: () => window.location.assign("/account/profile"),
...
렌더러(renderer)는 오직 이렇게 등록된 액션들만 해결(resolve)합니다:
// GeneratedUI.tsx
import type { UINode } from "./ui-contract";
import { actions } from "./actions";
...
권한 부여(Authorization)는 여전히 목적지 API에 의해 강제되어야 합니다. 액션을 숨기거나 매니페스트에서 누락시키는 것은 접근 제어(access control)가 아닙니다.
3. 아무도 보기 전에 리비전(revision)을 저장하기
생성된 응답은 불변의 리비전(immutable revision)이 되어야 합니다. 단일 current_ui JSON 컬럼을 덮어쓰지 마십시오. 이는 지원(support)에 필요한 증거를 파괴하기 때문입니다.
CREATE TABLE ui_revisions (
id text PRIMARY KEY,
scope text NOT NULL,
...
먼저 파싱(parse)하고, 그다음에 정책(policy)을 적용하며, 그 후에만 제안(proposal)을 저장하십시오:
const parsed = UIManifestSchema.safeParse(modelOutput);
if (!parsed.success) {
...
정의된 개인정보 보호 및 보관 이유가 없는 한, 이 테이블에 원본 프롬프트(raw prompt)를 포함하지 마십시오. 지원 업무에는 일반적으로 결과물인 매니페스트와 정책 결정 사항이 필요하며, 사용자 컨텍스트의 무기한 아카이브가 필요한 것은 아닙니다.
4. 피드백이 경로(route)뿐만 아니라 인터페이스를 식별하도록 만들기
두 명의 방문자가 서로 다른 레이아웃을 받을 수 있는 상황에서 /account와 같은 경로(route)만으로는 불충분합니다. 렌더링된 화면에 리비전 ID(revision ID)를 표시하십시오:
export function AdaptiveScreen({
revisionId,
root,
...
단순히 "이 경험은 어떠셨나요?"라고 묻기보다는, 분류(triage)에 도움이 되는 카테고리를 사용하십시오.
function FeedbackForm({ revisionId }: { revisionId: string }) {
async function submit(formData: FormData) {
await fetch("/api/ui-feedback", {
...
서버에서도 동일한 필드들을 검증(Validate)하십시오. 또한 revisionId가 존재하는지 확인해야 합니다. 클라이언트가 제공한 매니페스트(manifest)를 절대 신뢰하지 마십시오.
5. 보고서 생성을 중단해야 할 시점 결정하기
모든 불만 사항이 전역 롤백(rollback)을 필요로 하는 것은 아닙니다. 작은 결정 테이블(decision table)을 사용하면 대응을 비례적으로 유지할 수 있습니다:
| 신호 (Signal) | 즉각적인 조치 (Immediate action) | 후속 조치 (Follow-up) |
|---|---|---|
| 주관적인 문구 불만 1건 | 해당 리비전(revision)을 활성 상태로 유지 | 일반적인 분류(triage) 과정에서 검토 |
| ... |
중요한 차이점은 **범위(scope)**입니다. 결제 레이아웃이 깨졌다고 해서 반드시 저위험 대시보드에서의 실험을 중단할 필요는 없지만, 추가적인 결제 변이(mutation)는 중단해야 합니다.
롤백은 별도의 모델 요청이 아닌, 하나의 데이터베이스 트랜잭션(transaction)으로 이루어져야 합니다:
BEGIN;
UPDATE ui_revisions
...
만약 두 번째 업데이트가 영향을 미치는 행(row)이 0개라면, 이미 다른 리비전이 활성화되어 있을 수 있습니다. 롤백이 성공했다고 조용히 처리하는 대신, 해당 충돌(conflict)을 운영자에게 반환하십시오.
6. 지원 팀에게 스크린샷 찾기 대신 재생 뷰(replay view)를 제공하기
다음과 같은 내부 경로(route)를 생성하십시오:
/support/ui-revisions/:revisionId
이 뷰는 다음을 보여주어야 합니다:
- 검증된 매니페스트 (validated manifest)
- 부모 및 현재 상태
- 정책 결과 (policy result)
- 해당 리비전에 첨부된 피드백
- 동일한 컴포넌트 레지스트리(component registry)를 사용한 렌더링
- 명시적인 동결 (Freeze) 및 마지막 정상 상태로 복구 (Restore last good) 컨트롤
- 해당 리비전이 여전히 어떤 범위(scope)에 할당되어 있는지 여부
재생 뷰(replay view)는 기본적으로 읽기 전용(read-only) 상태를 유지해야 합니다. 과거의 매니페스트를 렌더링하는 것이 분석(analytics), 탐색(navigation), 결제 작업 또는 기타 실제 동작을 트리거해서는 안 됩니다. 액션 핸들러(action handlers)를 Would invoke: cancel_subscription (실행 시: 구독 취소됨)과 같은 라벨로 대체하십시오.
이는 경험이 적은 개발자들에게도 유용한 엔지니어링 관행입니다. 그들에게 "AI"를 신뢰하거나 거부하라고 요구하는 대신, 구체적인 결과물(artifact)을 검사하고, 위반된 불변성(invariant)을 식별하며, 제한된 범위 내에서 결정을 내리게 할 수 있습니다. 판단력은 자동화가 이해의 필요성을 제거한 척하는 것이 아니라, 실패 사례를 검토하는 과정에서 성장합니다.
7. 지원 루프(support loop)에서 AI의 위치
보고서가 불변 리비전(immutable revisions)에 연결되면, AI는 그 출력이 권고 사항(advisory)으로 남는 작업들을 도울 수 있습니다:
- 동일한 리비전과 예상되는 동작을 참조하는 보고서들을 클러스터링(cluster)하기;
- 리비전과 그 부모 리비전 간의 차이점을 요약하기;
- 기존의 컴포넌트 계약(component contract)을 사용하여 더 명확한 라벨을 제안하기;
- 사람이 승인할 수 있도록 리플레이(replay) 케이스를 제안하기.
AI는 다음과 같은 행동을 해서는 안 됩니다:
- 문구가 비슷해 보인다는 이유로 보고서를 종결하기;
- 교체 리비전(replacement revision)을 활성화하기;
- 새로운 컴포넌트를 렌더링하기 위해 패키지를 발명하거나 설치하기;
- 결제나 동의 변경이 무해하다고 결정하기;
- 보고서를 리플레이하는 동안 동작을 실행하기.
모델은 읽기 및 초안 작성 작업을 줄여줄 수 있습니다. 인터페이스가 무엇을 의미하도록 허용할지는 여전히 사람이 결정합니다.
선택 사항: 롤백(rollback)과 결합하지 않고 실시간 대화 추가하기
구조화된 양식은 집계(aggregation)에 유용하지만, 일부 사용자에게는 대화가 필요할 수 있습니다. 리비전 저장 및 롤백 기능을 애플리케이션 내부에 유지하면서 호스팅된 채팅 위젯을 추가할 수 있습니다.
예를 들어, Knocket은 스크립트 태그로 설치할 수 있는 임베디드 실시간 채팅 위젯을 제공하며, 별도의 커스텀 채팅 백엔드를 요구하지 않습니다. 방문자는 채팅을 시작하기 위해 계정이 필요하지 않습니다.
문서화되지 않은 위젯 메타데이터에 의존하기보다, 리비전 참조를 눈에 띄게 유지하고 복사하기 쉽게 만드십시오:
<p>
진단 코드:
<code id="ui-diagnostic">ui_revision=ui_01JABC123</code>
...
출시 전 실행해야 할 실패 드릴 (Failure drills)
모델이 알 수 없는 동작을 명명하는 경우
기대 결과: 스키마 검증(schema validation)이 전체 제안을 거부해야 합니다. 버튼을 조용히 누락시키거나 불완전한 화면을 렌더링하지 마십시오.
리비전이 스키마 검증을 통과했지만 중요한 동작을 숨기는 경우
스키마 유효성이 제품의 정확성을 증명하지는 않습니다. 결제 취소 여정(billing cancellation journey) 중 어딘가에 cancel_subscription을 요구하는 것과 같이, 범위별 정책 규칙(scope-specific policy rules)을 추가하십시오.
지원팀이 조사하는 동안 현재 리비전이 변경되는 경우
보고서는 반드시 원래의 불변 리비전(immutable revision)을 계속 가리키고 있어야 합니다. 재생(replay) 페이지에는 해당 리비전이 더 이상 활성 상태가 아님을 명확하게 명시해야 합니다.
피드백 엔드포인트(feedback endpoint)를 사용할 수 없는 경우
사용자가 다른 지원 채널에 포함할 수 있도록 진단 코드(diagnostic code)를 계속 표시하십시오. 서버가 수신을 확인하기 전까지는 보고서가 접수되었다고 주장하지 마십시오.
생성된 인터페이스의 렌더링에 실패하는 경우
생성된 서브트리(subtree) 외부에 에러 경계(error boundary)를 설치하십시오. 이는 서브트리를 정적인 검증된 내비게이션 표면(static known-good navigation surface)으로 교체하고 리비전 ID를 노출해야 합니다.
과거 재생(historical replay)이 실제 동작을 호출하는 경우
이를 출시 차단 요소(release blocker)로 취급하십시오. 재생 모드는 프로덕션 동작 레지스트리(production action registry)를 가져오는 대신 비활성 핸들러(inert handlers)를 주입해야 합니다.
모델이 새로운 의존성(dependency)을 제안하는 경우
런타임(runtime) 시에는 해당 제안을 무시하십시오. 만약 해당 기능이 진정으로 필요하다면, 레지스트리 검증(registry verification), 소유권 검토(ownership review), 락파일(lockfile) 변경, CI, 그리고 다른 의존성과 동일한 코드 리뷰 프로세스를 통해 패키지를 평가하십시오.
출시 체크리스트
특정 범위에 대해 적응형 UI(adaptive UI)를 활성화하기 전에 다음 사항을 확인하십시오:
- 모델 출력(Model output)은 데이터로 파싱되어야 하며, 코드로 평가되어서는 안 됩니다.
- 컴포넌트(Components)와 액션 ID(action IDs)는 명시적인 허용 목록(allowlists)에서 가져와야 합니다.
- 서버 측 권한 부여(Server-side authorization)는 가시성(visibility)과 독립적으로 유지되어야 합니다.
- 표시되는 모든 매니페스트(manifest)는 불변의 리비전 ID(immutable revision ID)를 가져야 합니다.
- 피드백 저장소는 제출된 UI JSON을 신뢰하는 대신 해당 리비전 ID를 저장해야 합니다.
- 지원 팀은 비활성 액션(inert actions)이 포함된 정확한 매니페스트를 재생(replay)할 수 있어야 합니다.
- 생성이 활성화되기 전에 검증된 안정적인 리비전(known-good revision)이 존재해야 합니다.
- 롤백(Rollback) 시에는 모델을 호출하지 않아야 합니다.
- 고위험 범위(High-risk scopes)에는 더 엄격한 활성화 규칙을 적용해야 합니다.
- 의존성 추가(Dependency additions)는 저장소 및 CI의 결정으로 유지되어야 합니다.
- 생성된 UI를 렌더링할 수 없는 경우 사용자를 위한 폴백 경로(fallback route)가 있어야 합니다.
- 운영자는 범위(scope)별로 생성을 비활성화할 수 있어야 합니다.
적응형 인터페이스(Adaptive interfaces)가 프론트엔드나 지원 업무를 없애는 것은 아닙니다. 이는 모든 화면을 수동으로 배치하는 작업에서, 계약(contracts)을 정의하고, 안전하지 않은 의미론(unsafe semantics)을 인식하며, 제안이 실패했을 때 적절히 대응하는 방향으로 업무를 전환하는 것입니다.
이는 개발자의 가치가 상실되는 것이 아닙니다. 오히려 개발자의 판단이 어디에 필요한지를 더 명확하게 설명해 주는 것입니다.
공지: 저는 Knocket에서 일하고 있으므로, 이를 중립적인 권장 사항이라기보다 하나의 구현 사례로 간주해 주시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기