AIKit으로 구축된 채팅 예시(라이트 테마)

모든 것을 지배하는 하나의 채팅: Gravity UI 기반 AI 어시스턴트 라이브러리를 구축했습니다
모든 것을 지배하는 하나의 채팅: Gravity UI 기반 AI 어시스턴트 라이브러리를 구축했습니다
AIKit 라이브러리 출시에 관한 기사: 개발 중에 집중한 부분, 필요한 이유, 그리고 자체 프로젝트에서 사용하는 방법.
지난 1년 동안 AI 도우미 붐을 목격해 왔고, 이는 Yandex Cloud의 인터페이스에도 영향을 미쳤습니다. 어느 날은 기술 지원에 모델 기반 챗봇이 등장했고, 또 어느 날은 콘솔에 업무 운영을 위한 에이전트가 생겼습니다. 각 팀은 모델을 연결하고, 대화 로직을 설계하고, 디자인을 그린 뒤, 채팅 UI를 구현했는데 — 이 모든 것을 각자 따로 해왔습니다.
여러 팀이 공통 프레임워크인 Gravity UI 위에서 인터페이스를 만들었지만, 시간이 지나며 변형이 너무 많아져 일관된 사용자 경험을 유지하기가 어려워졌습니다. 게다가 동료들은 점점 더 같은 문제를 반복해서 해결하느라 시간을 쓰고 있었습니다.
매번 바퀴를 새로 발명하지 않기 위해, 우리는 축적된 실무 경험을 하나의 접근 방식으로 정리하고 AI 챗봇을 위한 도구 — @gravity‑ui/aikit — 를 만들었습니다. 이 도구를 쓰면 며칠 만에 완전한 어시스턴트 인터페이스를 만들 수 있고, 동시에 다양한 시나리오에 맞게 쉽게 адаптировать (커스터마이즈)할 수 있습니다.

제 이름은 일리야 롬테프이며, Yandex Cloud의 Foundation Services 팀에서 시니어 개발자로 일하고 있습니다. 이 글에서는 우리가 왜 AIKit을 만들기로 했는지, AIKit이 어떻게 구성되어 있는지, 앞으로의 계획을 조금 — 그리고 여러분이 직접 무엇을 시도해 볼 수 있는지도 — 이야기하겠습니다.
AIKit을 어떻게, 왜 만들었는가
지난 1년 동안 Yandex Cloud에서는 AI 어시스턴트를 제공하는 서비스 수가 늘었습니다. 예를 들면 다음과 같습니다:
-
SourceCraft의 Code Assistant Chat — 이 어시스턴트는 개발자가 코드를 작성하도록 돕고, AI 에이전트 모드에서는 리포지토리를 생성·설정하고, CI/CD 프로세스를 실행하며, 문서 관련 질문에 답하고, 업무를 자동화합니다. 또한 이슈와 풀 리퀘스트를 관리할 수 있고, 코드 작업(설명, 파일 생성 및 편집)도 수행합니다.
-
클라우드 콘솔의 AI 어시스턴트 — Yandex Cloud에서 리소스를 관리하기 위해 개발된 어시스턴트입니다. 주요 목표는 API와 도구와의 상호작용 복잡성을 숨기면서, 클라우드 인프라를 빠르고 안전하게 설정·변경·운영하도록 돕는 것입니다.
에코시스템 내에 대략 열 개의 채팅이 생겼고, 각자 고유한 로직, 메시지 포맷, 그리고 코너 케이스 집합을 가지고 있었습니다.
우리는 팀들이 대체로 비슷한 과제 묶음에 도달한다는 것을 발견했습니다. 대부분이 필요로 하는 것은 다음과 같습니다:
-
사용자와 어시스턴트 메시지를 깔끔하게 표시하기,
-
답변 스트리밍을 올바르게 구성하기,
-
“어시스턴트가 입력 중” 인디케이터 표시하기,
-
연결 끊김이나 재시도(retry) 같은 오류 처리하기.
문제의 본질은 같은데 해결 방식은 많고 UX가 서로 달랐습니다. 예를 들어 채팅 히스토리의 위치와 표시 방식이 그렇습니다. 메뉴처럼 열리는 별도의 화면일 수도 있고, 팝업 안의 채팅 목록일 수도 있습니다.
문제가 드러났습니다. 서로 다른 채팅에서의 경험이 크게 달랐습니다. 어떤 곳에서는 어시스턴트가 답을 스트리밍했고, 어떤 곳에서는 완성된 텍스트를 즉시 보여주었습니다. 한 인터페이스에서는 메시지가 그룹화되었고, 다른 곳에서는 연속적인 피드로 흘러갔습니다. 이는 공통 UX를 깨뜨렸습니다 — 사용자가 같은 에코시스템의 제품 사이를 이동할 때, 어시스턴트에 대한 체감이 완전히 달라졌던 것입니다.

또한 모델에 새로운 기능을 롤아웃하는 일도 점점 더 어려워지고 있다는 것이 заметно (명확)해졌습니다. 사용자에게 예를 들어 도구 사용 능력(tooling), 멀티모달리티, 혹은 tool의 구조화된 응답을 제공하려면 컨트랙트를 합의하고, 백엔드를 보완한 뒤, 각 팀이 따로 UI를 업데이트해야 했습니다. 이런 조건에서는 어떤 변경이든 시간이 많이 들고 확장성이 떨어졌습니다.
우리는 이 변형의 증가를 멈추고 다시 예측 가능성을 되찾고 싶었습니다. 이를 위해 데이터 모델과 작업 패턴을 통일하고, 팀들이 0에서 시작하지 않도록 готовые (준비된) 컴포넌트와 훅을 제공하며, 커스터마이즈를 위한 여지도 남겨야 했습니다 — 각자의 시나리오가 다르기 때문입니다.
그렇게 해서 @gravity‑ui/aikit라는 별도 라이브러리 아이디어에 도달했습니다. 이는 Gravity UI의 확장이며 같은 원칙을 따르지만, 대화, 어시스턴트, 멀티모달리티 같은 현대적인 AI 시나리오에 초점을 맞춥니다.
AIKit 아키텍처: 무엇을 기반으로 했는가
AIKkit을 설계할 때 우리는 AI SDK의 경험과 몇 가지 фундаментальный (근본) 원칙을 기준으로 삼았습니다.
Atomic Design을 기반으로: 라이브러리 전체를 원자(Atom)에서 페이지까지 쌓아 올리는 구조로 만들었습니다. 이런 구조는 명확한 계층을 제공하고, 컴포넌트를 재사용할 수 있게 하며, 필요하다면 어느 레벨에서든 동작을 변경할 수 있습니다.

완전히 SDK-agnostic: AIKit은 특정 AI 프로바이더에 의존하지 않습니다. OpenAI, Alice AI LLM, 혹은 자체 백엔드를 사용할 수 있습니다 — UI는 props로 데이터를 받으며, 상태(state)와 요청(request)은 제품(서비스) 쪽에 남습니다.
복잡한 시나리오를 위한 두 가지 사용 레벨: “바로 사용 가능한” готовый (완성형) 컴포넌트가 있고, UI를 완전히 제어할 수 있는 로직 훅도 있습니다. 예를 들어 PromptInput을 쓰거나, usePromptInput 기반으로 자체 입력 필드를 만들 수 있습니다. фундамент (기초)을 다시 쓰지 않고도 유연성을 얻을 수 있습니다.
확장 가능한 타입 시스템. 일관성과 타입 안정성을 보장하기 위해 확장 가능한 데이터 모델을 만들었습니다. 메시지는 하나의 타입화된 구조로 표현됩니다. 사용자 메시지, 어시스턴트 메시지, 그리고 몇 가지 기본 콘텐츠 타입 — 텍스트(text), 모델의 사고 과정(thinking), 도구(tool) — 이 있습니다. 또한 MessageRendererRegistry를 통해 собственные (자체) 타입을 추가할 수 있습니다.
모든 것은 TypeScript로 타입이 지정되어 있어, 복잡한 시나리오를 더 빠르게 구성하고 개발 단계에서 오류를 피하는 데 도움이 됩니다.
// 1. 데이터 타입을 정의합니다
type ChartMessageContent = TMessageContent<
'chart',
{
chartData: number[];
chartType: 'bar' | 'line';
}
>;
// 2. 렌더링 컴포넌트를 만듭니다
const ChartRenderer = ({part}: MessageContentComponentProps<ChartMessageContent>) => {
return <div>차트 시각화: {part.data.chartType}</div>;
};
// 3. 렌더러를 등록합니다
const customRegistry = registerMessageRenderer(createMessageRendererRegistry(), 'chart', {
component: ChartRenderer,
});
// 4. AssistantMessage에서 사용합니다
<AssistantMessage message={message} messageRendererRegistry={customRegistry} />;
마지막으로 CSS 변수 기반의 테마 지원을 предусмотрели (반영)했고, i18n (RU/EN)을 추가했으며, 접근성(ARIA, 키보드 네비게이션)을 확보했고, Docker에서 Playwright Component Testing으로 비주얼 회귀 테스트를 설정했습니다 — 이렇게 해서 라이브러리는 프로덕션 사용 준비가 끝났습니다.
내부 구성
AIKit의 기반은 단일화된 대화 모델입니다. 이를 만들기 위해 먼저 메시지 계층 구조를 정리해야 했습니다.
메시지는 그 자체로 꽤 다면적인 엔티티입니다. LLM에서 오는 첫 번째 메시지는 하나의 스트림입니다. 하지만 그 안에는 다양한 중첩 메시지가 있을 수 있습니다. 본질적으로는 하나의 질문을 해결하기 위한 추론, 제안, tool 호출입니다. 이런 서로 다른 하위 메시지들은 사실상 백엔드에서 오는 하나의 메시지입니다. 하지만 간단한 LLM 사용에서는 각 하위 메시지가 별도의 메시지가 될 수도 있습니다.
그래서 채팅을 두 방식 모두로 쓸 수 있게 했습니다. 메시지는 서로 중첩될 수도 있고, 플랫할 수도 있습니다 — 필요에 따라 달라집니다.
상태 관리는 서비스 쪽에 남습니다. AIKit은 데이터를 자체 저장하지 않고 외부에서 받습니다. 팀은 React State, Redux, Zustand, Reatom 등 편한 것을 무엇이든 사용할 수 있습니다. 우리는 типовая (전형적인) UI 로직을 캡슐화한 훅을 제공할 뿐입니다. 예를 들어:
-
useSmartScroll을 통한 스마트 스크롤; -
useDateFormatter로 로케일을 고려한 날짜 포맷팅 등 날짜 처리; -
useToolMessage로 tool 메시지 처리; -
그리고 대화를 구성하는 데 필요한 기타 모든 것.
또한 AIKit은 확장 가능하게 유지됩니다. 어떤 모델이든 연결할 수 있고, 자체 콘텐츠 타입을 만들 수 있으며, 훅의 로직을 사용하거나 готовые (기성) 컴포넌트를 기반으로 자신만의 UI를 полностью (완전히) 구축할 수 있습니다. 아키텍처는 공통 원칙을 깨지 않으면서 실험할 수 있게 해줍니다.
나만의 채팅을 만드는 방법
첫 번째 채팅을 만들기 위해 준비된 컴포넌트 ChatContainer를 사용해 보겠습니다:
import React, { useState } from 'react';
import { ChatContainer } from 'aikit';
import type { ChatType, MessageType } from 'aikit';
function App() {
const [messages, setMessages] = useState<MessageType[]>([]);
const [chats, setChats] = useState<ChatType[]>([]);
const [activeChat, setActiveChat] = useState<ChatType | null>(null);
const handleSendMessage = async (content: string) => {
// 메시지 전송 로직
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: content })
});
const data = await response.json();
// 상태 업데이트
setMessages(prev => [...prev, data]);
};
return (
<ChatContainer
messages={[]}
onSendMessage={() => {}}
welcomeConfig={{
description: '메시지를 입력하거나 제안 항목을 선택해 대화를 시작하세요.',
image: <Icon data={() => {}} size={48}/>,
suggestionTitle: '이렇게 물어보세요:',
suggestions: [
{
id: '1',
title: '양자 컴퓨팅을 쉽게 설명해줘'
},
{
id: '2',
title: '자연에 관한 시를 써줘'
},
{
id: '3',
title: '내 JavaScript 코드 디버깅을 도와줘'
},
{
id: '4',
title: '최근 AI 동향을 요약해줘'
}
],
title: 'AI 채팅에 오신 것을 환영합니다'
}}
/>
);
}
“기본 제공(out of the box)” 상태는 이렇게 보입니다:

조금 더 праздничный (축제) 분위기를 더해봅시다:
-
초기 상태를 손봅니다.
더 세밀한 설정을 위해 채팅을 отдельные (개별) 컴포넌트로 조립해보겠습니다:
Header,MessageList,PromptBox.import { Header, MessageList, PromptBox } from 'aikit'; function CustomChat() { return ( <div className="custom-chat"> <Header title="AI Assistant" onNewChat={() => {}} /> <MessageList messages={messages} showTimestamp /> <PromptBox onSend={handleSend} placeholder="무엇이든 물어보세요..." /> </div> ); } -
MessageType을 통해 가져온 다양한 내장 메시지 타입을 적용합니다.-
thinking— AI의 사고 과정을 보여줍니다(사용자는 어시스턴트가 어떤 논리로 답을 준비하는지 살펴볼 수 있습니다). -
tool— 인터랙티브한 응답 블록을 표시하는 데 적합합니다. 우리 경우에는 코드 블록이며, 문법 강조가 올바르게 동작하고 편집 및 클립보드 복사 기능을 지원합니다.
또한 이미지가 포함된 메시지 등 자체 타입을 추가할 수도 있습니다:
type ImageMessage = BaseMessage<ImageMessageData> & { type: 'image' }; const ImageMessageView = ({ message }: { message: ImageMessage }) => ( <div> <img src={message.data.imageUrl} /> {message.data.caption && <p>{message.data.caption}</p>} </div> ); const customTypes: MessageTypeRegistry = { image: { component: ImageMessageView, validator: (msg) => msg.type === 'image' } }; <ChatContainer messages={messages} messageTypeRegistry={customTypes} /> -
-
CSS로 스타일링을 추가하고…
…데드 모로즈(러시아의 산타)와 함께하는 채팅을 얻을 수 있습니다:)

개별 요소를 완전히 커스터마이즈하려면 훅을 사용할 수 있습니다 — 글 아래 댓글에서 여러분의 스타일링 버전도 꼭 보고 싶습니다!
AIKit이 서비스에 미친 영향
Yandex Cloud에서 AIKit을 사용한 결과는 빠르게 드러났습니다. 모든 서비스에서 어시스턴트의 동작이 одинаково (동일)해졌습니다: 답변을 동일하게 스트리밍하고, 오류를 동일하게 표시하고, 메시지를 동일하게 그룹화했습니다. UX가 일관되게 되었고, 이제 에코시스템 전체에서 взаимодействие (상호작용)하기 더 쉬우며, 동작이 더 ожидаемый (기대 가능)하고 예측 가능해졌습니다.
-
UX 언어가 하나로 통일되었습니다 — 서로 다른 제품의 어시스턴트 채팅이 이제 하나의 에코시스템 일부처럼 느껴집니다. 사용자는 예측 가능한 동작(동일한 스트리밍, 오류 처리, 상호작용 패턴)을 보게 됩니다.
-
채팅 UI 개발 속도가 훨씬 빨라졌습니다.
-
중앙집중식 발전 — thinking 콘텐츠 타입이나 tool 처리 개선 같은 새 기능을 한 번 추가하면 모두에게 자동으로 제공됩니다.
-
라이브러리는 에코시스템에서 AI 인터페이스 표준을 형성하는 기반이 되었습니다.
다음은 무엇인가
이제 계획을 말씀드리겠습니다. 몇 가지 방향을 выделили (선정)했습니다:
-
매우 큰 채팅 히스토리를 다루기 위한 가상화(virtualization)로 성능 개선.
-
активно (활발히) 발전하는 AI 에이전트의 새로운 기능을 지원하기 위한 기본 시나리오 확장.
-
인기 있는 AI 모델 데이터를 우리 채팅 데이터 모델로 매핑하는 작업을 단순화하기 위한 유틸리티 추가.
추가로 문서와 예제도 발전시킬 예정입니다. 그리고 물론 커뮤니티 성장 — 이 라이브러리가 사내뿐 아니라 외부 개발자에게도 유용하길 바랍니다.

일리야 롬테프
프론트엔드 개발자