사내 챗봇을 열고 “우리 서비스 요금제 세 개를 한눈에 비교해 줘”라고 친다. 잠시 뒤 길쭉한 마크다운 표가 주르륵 쏟아진다. 이어서 “오늘 저녁값 네 명이서 팁 15% 얹어 나누면 한 사람당 얼마야?”라고 묻는다. 이번에도 긴 줄글 설명 끝에 숫자 하나가 덩그러니 찍혀 나온다. 만약 팁을 20%로 바꾸거나 인원을 다섯 명으로 늘려 보고 싶다면 똑같은 질문을 숫자만 고쳐서 다시 타이핑해야 한다.

텍스트로만 돌아오는 답은 눈으로 읽을 수는 있어도 손으로 이리저리 만져 볼 수는 없다. 조건 하나를 바꿔 가며 결과를 비교하는 일까지 오롯이 사람이 프롬프트를 고쳐 치는 수고로 때워야 한다. 요즘 ChatGPT나 Claude가 채팅창 한가운데에 텍스트 대신 조작 가능한 차트나 계산기를 곧바로 띄워 주기 시작한 것도 바로 이 답답함 때문이다.

CopilotKit 팀이 공개한 ‘Open Intelligent UI’는 이 기능을 내 서비스에도 그대로 붙일 수 있게 풀어 놓은 오픈소스(MIT 라이선스) 프로젝트다. 사용자의 질문을 보고 언제 줄글 대신 화면을 띄울지 어떻게 결정하는지, 그리고 AI가 즉석에서 짠 화면 코드를 어떻게 안전하게 실행하는지 그 속내를 들여다본다.

질문에 따라 답의 생김새가 바뀐다

같은 채팅 안에서 질문에 따라 글, 표, 차트, 계산기, 지도 중 하나로 답하는 모습을 보여주는 그림
답의 내용만큼 답의 모양도 질문에 따라 달라진다.

이 프로젝트가 내건 한 줄짜리 소개 문구는 “만져 볼 수 있는 답변(Answers you can interact with)”이다. 에이전트는 사용자의 질문이 들어올 때마다 세 가지 길 중 하나를 고른다. 평범하게 줄글로 답하거나 앱에 미리 만들어 둔 UI 컴포넌트를 꺼내 채우거나 아예 그 질문에 딱 맞는 새 화면 코드를 즉석에서 짜서 띄우는 것이다.

공개된 데모를 몇 개 눌러 보면 감이 확 온다. 비행기 조종 원리를 물으면 3D 비행기 모델을 띄워 놓고 피치(위아래), 롤(좌우 기울기), 요(방향 전환) 슬라이더를 마우스로 직접 끌어 돌려 보게 한다. 요금제를 비교해 달라고 하면 먼저 깔끔한 표로 보여 준 뒤 버튼 하나로 같은 데이터를 막대 차트로 바꿔 준다. 총액과 팁 비율, 인원수를 슬라이더로 움직이면 즉시 1인당 금액이 바뀌는 더치페이 계산기나 번호가 매겨진 핀이 찍힌 해안도로 여행 지도도 채팅창 안에 쏙 들어온다.

재미있는 건 지도 데모 아래에 작게 붙어 있는 경고 문구다. 핀과 핀 사이를 이은 선은 이해를 돕기 위한 예시일 뿐 실제 내비게이션으로 검증된 운전 경로가 아니라고 못 박아 두었다. 화면이 아무리 그럴듯하고 매끄럽게 돌아가도 그 안에 담긴 사실관계까지 보장해 주지는 않는다는 뜻이다. 이렇게 만들어진 화면은 버튼 하나로 독립된 HTML 파일로 내려받아 따로 쓸 수도 있다.

답변이 줄글이 아니라 큼직한 화면으로 쏟아져 들어오면 채팅창의 스크롤 경험도 세심하게 다듬어야 한다. 이 앱은 답변이 실시간 스트리밍으로 길어지는 동안에도 사용자가 읽고 있던 화면 위치를 멋대로 밀어내지 않고 그대로 잡아 준다. 복사 버튼 같은 부가 기능도 평소에는 숨겨 뒀다가 마우스를 올리거나 키보드 포커스가 갔을 때만 조용히 나타나고 마우스가 없는 모바일 터치 화면에서는 처음부터 보이게 처리했다.

표현 방식을 고르는 모델과 내용을 채우는 모델

사용자 질문이 Jev로 가서 표현 형식이 정해지고 기본 표는 A2UI로, 차트와 계산기와 지도는 샌드박스 HTML로 나뉘는 흐름을 보여주는 그림
무엇을 말할지와 어떻게 보여줄지를 다른 모델이 맡는다.

내부 구조를 뜯어보면 답변 하나를 만드는 데 두 개의 모델이 이어달리기를 한다. 질문이 들어오면 먼저 TypeSafe의 라우팅 전용 모델인 Jev(jev-latest)가 나서서 이번 질문에는 어떤 모양의 그릇이 가장 어울리는지부터 판정한다. 줄글로 할지, 표로 그릴지, 인터랙티브 차트로 띄울지 형식부터 고르는 셈이다. 그 결정이 끝나면 본타석에 들어선 답변 모델(기본값은 OpenAI의 chat-latest)이 그 그릇에 맞춰 실제 알맹이를 채워 넣는다.

굳이 이렇게 역할을 둘로 쪼갠 이유는 어렵지 않게 짐작할 수 있다. 모델 하나에게 답변 내용도 생각하고 UI 형식까지 동시에 고르라고 시키면 늘 하던 습관대로 줄글부터 주절주절 쏟아내기 십상이다. 앞단에서 가벼운 모델이 “이번 판은 계산기 UI로 간다”고 틀을 먼저 못 박아 주면 뒷단의 답변 모델은 딴생각 없이 그 틀을 채우는 데만 집중할 수 있다.

앞단에서 정해진 형식에 따라 화면을 실제로 그려 내는 엔진도 두 갈래로 나뉜다. 단순한 비교 표 같은 건 ‘A2UI’라는 규격으로 그리고 복잡한 차트나 다이어그램, 계산기, 지도는 HTML과 CSS, JavaScript 코드를 즉석에서 생성해 그린다.

이 모든 판단과 코드 생성 결과물은 다 만들어질 때까지 기다리지 않고 실시간 스트리밍으로 흘러 들어온다. 파이썬 백엔드 에이전트가 뱉어 내는 흐름을 CopilotKit이 받아서 Next.js 프런트엔드로 토스하는 구조다. 이때 백엔드와 프런트엔드 사이에서 주고받는 메시지 규약을 ‘AG-UI’가 잡아 준다. 덕분에 프런트엔드 화면 코드는 뒤에서 어떤 에이전트 프레임워크가 돌아가는지 신경 쓸 필요 없이 똑같은 방식으로 화면을 받아 그릴 수 있다.

저장소 README를 읽다 보면 개발팀의 단호한 철학이 느껴지는 대목이 하나 나온다. 만약 Jev나 OpenAI 같은 외부 모델 API에서 장애가 나면 다른 대체 모델로 몰래 갈아타지(fallback) 않고 그냥 화면에 에러를 그대로 띄워 버린다는 원칙이다. 어설픈 대체 모델로 조용히 넘어가 버리면 겉으로는 멀쩡히 돌아가는 것처럼 보여도 정작 지금 화면을 누가 어떻게 그렸는지 알 수 없게 되기 때문이다.

안전한 조립식 부품과 격리된 즉석 코드

A2UI는 데이터를 받아 앱의 준비된 컴포넌트로 그리고 샌드박스 HTML은 생성된 코드를 격리된 iframe에서 실행한다는 차이를 비교한 그림
자유로울수록 가둬 둘 벽이 필요하다.

앞서 잠깐 언급한 A2UI는 Google이 제안한 UI 데이터 규격이다. 쉽게 말해 AI가 실행 가능한 코드를 직접 짜서 보내는 대신 JSON 같은 순수 데이터로 “여기엔 표를 놓고 첫 칸엔 이 글자를 넣어라” 하고 설계도만 보내는 방식이다. 그러면 프런트엔드 앱이 그 설계도를 읽어서 미리 안전하게 만들어 둔 자체 리액트 컴포넌트로 화면을 조립한다. AI가 할 수 있는 일은 앱에 이미 준비된 블록을 골라 배치하는 것뿐이라 디자인 톤앤매너가 깨질 일도 없고 악성 스크립트가 실행될 위험도 원천 차단된다.

하지만 더치페이 계산기나 3D 비행기 시뮬레이터처럼 질문마다 전혀 다른 동작이 필요한 화면은 미리 만들어 둔 컴포넌트 몇 개로 조립할 수가 없다. 그래서 이럴 때는 generateSandboxedUi라는 도구가 나서서 생짜 HTML과 CSS, JavaScript 코드를 그 자리에서 직접 짠다. 물론 AI가 짠 자바스크립트 코드를 내 웹페이지에서 그냥 실행하면 큰일 난다. 그래서 이 코드는 바깥 페이지와 철저히 분리된 샌드박스 iframe(웹페이지 안에 또 다른 작은 웹페이지를 가둬 두는 액자) 안에서만 돌아간다. 슬라이더를 밀고 계산을 돌리는 모든 동작이 그 좁은 유리방 안에서만 이뤄진다.

심지어 AI가 화면 코드를 뱉어 내는 순서까지 엄격하게 못 박아 두었다. 반드시 initialHeight(초기 높이) → placeholderMessages(로딩 안내 문구) → css → html → jsFunctions → jsExpressions 순서대로 생성해야 한다. 액자의 세로 높이와 로딩 문구가 제일 먼저 도착하기 때문에 화면이 덜컥거리며 깨지지 않고 부드럽게 자리를 잡은 뒤 스타일과 알맹이가 차례로 채워진다.

이 화면 생성 엔진은 굳이 이 채팅 앱 전체를 다 쓰지 않아도 따로 떼어 쓸 수 있다. 저장소 안의 apps/mcp/ 폴더를 보면 독립된 MCP(Model Context Protocol) 서버가 들어 있어서 다른 에이전트 도구에서도 assemble_document 함수를 불러 똑같이 HTML 화면을 찍어 낼 수 있다. 다만 이 MCP 서버가 돌려주는 건 어디까지나 텍스트로 된 HTML 코드 덩어리뿐이다. 그 코드를 안전한 샌드박스 iframe에 가두고 바깥 앱과 통신 다리를 놓는 작업은 가져다 쓰는 쪽에서 직접 구현해야 한다.

화면에서 누른 버튼이 다음 프롬프트가 된다

iframe 안에서 사용자가 값을 고르면 검증된 호스트 브리지를 거쳐 에이전트의 다음 턴이 시작되고 새 화면이 따로 쌓이는 순환을 보여주는 그림
다음 질문을 타이핑하지 않고 화면에서 고른다.

아무리 멋진 화면이 떠도 거기서 끝이라면 일방적인 대시보드에 불과하다. 이 프로젝트의 진짜 묘미는 화면 안에서 사용자가 조작한 결과가 다시 채팅의 다음 질문으로 자연스럽게 이어진다는 점이다. 사용자가 위젯 안의 버튼을 누르면 그때 고른 값들이 엄격하게 검증된 ‘호스트 브리지(Host Bridge)’라는 좁은 통로를 거쳐 바깥 채팅 앱으로 전달되고 그 값이 곧바로 에이전트의 다음 턴을 깨운다.

물론 슬라이더를 좌우로 움직이거나 탭을 바꿔 보는 것처럼 자잘한 조작을 할 때마다 매번 비싼 AI 모델을 부르지는 않는다. 그런 계산은 iframe 안의 자바스크립트가 즉시 처리하고 사용자가 최종 결정 버튼을 눌렀을 때만 에이전트를 호출해 대화를 이어 간다.

한 가지 흥미로운 설계 선택은 이미 그려진 이전 화면을 제자리에서 고쳐 쓰는(patch) 기능을 아예 두지 않았다는 점이다. 새로운 턴이 시작되면 위쪽의 옛날 화면은 그대로 박제해 두고 아래쪽에 새 화면을 새로 그린다. 예컨대 “요금제를 표로 비교해 줘” 다음에 “이걸 차트로 바꿔 줘”라고 말하면 위쪽의 표는 그대로 남고 아래에 차트가 새로 붙는 식이다. 여러 턴에 걸쳐 화면 상태를 부분 업데이트하려다 코드가 꼬이는 걸 막으려는 실용적인 타협으로 보인다.

첫 화면에 떠 있는 추천 질문 카드를 눌러 봐도 이 흐름을 바로 체험할 수 있다. “내 취향에 맞는 주말 여행지 찾기”나 “공정하게 밥값 나누기” 카드를 누르면 미리 저장해 둔 녹화 화면이 뜨는 게 아니라 실제로 뒤에서 에이전트가 돌아가며 위젯을 생성하고 거기서 고른 선택지가 꼬리에 꼬리를 물며 다음 대화로 이어진다.

내 서비스에 붙이기 전에 따져 볼 것들

결국 Open Intelligent UI의 아키텍처는 세 가지 깔끔한 역할 분담으로 요약된다. 어떤 모양으로 보여 줄지 고르는 모델과 실제 내용을 채우는 모델을 나눴고 검증된 고정 컴포넌트(A2UI)와 자유로운 즉석 코드(iframe)를 나눴으며 화면 내부의 가벼운 조작과 무거운 에이전트 재호출을 나눴다. 사내 챗봇에 생성형 UI를 도입하려는 팀이라면 이 세 가지 경계선을 그대로 참고해 설계도를 그려 볼 만하다.

실제 기술 스택은 프런트엔드에 Next.js와 CopilotKit을, 백엔드에 Python Deep Agent와 FastAPI를 두고 둘 사이를 AG-UI 프로토콜로 묶었다. 로컬에서 직접 돌려 보려면 Node.js 22 이상과 pnpm 9, Python 3.12와 uv가 깔려 있어야 한다. 기본 설정대로 실행하려면 OpenAI와 TypeSafe 양쪽의 유료 API 키가 모두 필요하고 여행 지도 같은 위젯이 외부 지도 타일과 사진을 불러올 수 있게 네트워크도 열려 있어야 한다.

데모 화면 상단에는 방문자가 자기 API 키를 직접 입력해 써 보는 기능도 들어 있다. 입력한 키는 서버에 저장하지 않고 브라우저 메모리에만 잠시 올려 둔다고 설명하지만 README에서도 이 기능은 반드시 신뢰할 수 있는 내부 배포 환경에서만 켜 두라고 경고한다.

글을 마무리하며 README에 적힌 문장 하나를 다시 떠올려 본다. 코드 빌드와 테스트가 전부 통과했다고 해서 실제 모델 API 연결이나 운영 배포까지 검증된 건 아니라는 당연한 경고다. AI가 그려 주는 화려한 인터랙티브 화면도 똑같다. 슬라이더가 매끄럽게 움직이고 차트가 예쁘게 그려졌다고 해서 그 안에 들어 있는 계산 공식이나 여행 경로가 무조건 정답이라는 법은 없다. 화려한 UI는 답을 훨씬 편하게 만져 보게 도와줄 뿐, 그 답이 진짜로 맞는지는 여전히 사람이 눈을 부릅뜨고 확인해야 한다.

출처: CopilotKit/OpenIntelligentUI, GeekNews 소개글