로컬 AI 플랫폼을 위한 실시간 웹 UI 구축 — Angular 22 + Spring Boot 4
요약
로컬 AI 플랫폼 Jarvis의 7단계 개발 과정으로, 터미널 기반 시스템을 Angular와 Spring Boot를 활용해 브라우저 기반 웹 UI로 확장하는 방법을 다룹니다. Spring WebFlux의 반응형 스트림과 Angular의 RxJS를 결합하여 효율적인 실시간 스트리밍 인터페이스를 구축하는 과정을 설명합니다.
핵심 포인트
- Spring WebFlux와 Angular(RxJS)의 조합을 통한 반응형 스트리밍 구현
- SSE 구현 시 EventSource의 한계(POST 미지원, 헤더 제약) 극복 필요성
- 유지보수 효율성을 고려한 프레임워크 선택 전략
- Angular Material을 커스텀 SCSS로 디자인하여 독자적인 UI 구축
터미널에서 브라우저로 — Jarvis AI 플랫폼의 7단계.
우리가 멈췄던 지점
백엔드 중심의 6단계를 완료한 후, Jarvis는 완전히 기능적인 로컬 우선 (local-first) AI 플랫폼으로 진화했습니다.
Phase 1 → AI Chat + JWT + CLI
Phase 2 → Long-Term Memory + pgvector
Phase 3 → RAG Document Search
...
모든 것이 작동했습니다.
하지만 모든 것이 터미널 안에 머물러 있었습니다.
7단계는 그것을 변화시킵니다.
목표는 프로젝트의 철학을 타협하지 않으면서도 현대적인 브라우저 기반 인터페이스를 구축하는 것입니다:
당신의 AI. 당신의 데이터. 당신의 머신.
프론트엔드 프레임워크 선택
단 하나의 컴포넌트를 작성하기 전에, 우리는 커뮤니티 투표를 진행했습니다.
선택지는 다음과 같았습니다:
- Angular
- React
- Vue
- Next.js
Angular가 승리했습니다.
세 가지 이유가 있었습니다.
1. 반응형 프로그래밍 (Reactive Programming)이 Spring WebFlux에 적합함
Jarvis는 Spring WebFlux를 기반으로 구축되었습니다.
백엔드는 이미 반응형 스트림 (reactive streams)과 서버 전송 이벤트 (Server-Sent Events, SSE)를 통해 통신합니다.
Angular는 이미 RxJS를 통해 이 모델을 수용하고 있어, 스트리밍 API를 구현하는 것이 자연스럽습니다.
구독 (subscriptions), 취소 (cancellation), 정리 (cleanup), 그리고 스트림 합성 (stream composition)을 관리하는 것이 React 훅 (hooks)을 수동으로 조정하는 것보다 훨씬 더 깔끔합니다.
2. 유지 관리자가 이미 Angular를 알고 있음
오픈 소스 프로젝트는 유지 관리자의 속도에 맞춰 움직입니다.
내가 이미 깊이 이해하고 있는 프레임워크를 사용하는 것은 다음을 의미합니다:
- 더 빠른 리뷰
- 더 빠른 버그 수정
- 더 쉬운 기여자 지원
기여자들은 언제든 Angular를 배울 수 있습니다.
유지 관리자가 모든 풀 리퀘스트 (pull request)를 리뷰하는 동안 완전히 새로운 생태계를 배워야 해서는 안 됩니다.
3. Angular Material을 사용하되 Angular Material처럼 보이지 않게 하기
Angular Material은 훌륭한 기반을 제공합니다:
- 다이얼로그 (dialogs)
- 아이콘 (icons)
- 폼 컨트롤 (form controls)
- 접근성 (accessibility)
하지만 전체적인 외형은 **커스텀 (custom)**입니다.
모든 레이아웃, 간격 규칙, 타이포그래피 선택, 그리고 컬러 팔레트는 SCSS를 통해 구현됩니다.
그 결과는 또 다른 기성 Material 애플리케이션이 아닌, Jarvis처럼 느껴집니다.
스택 (The Stack)
| 계층 (Layer) | 기술 (Technology) |
|---|---|
| 프레임워크 (Framework) | Angular 22 |
| ... |
첫 번째 놀라움: EventSource가 작동하지 않음
Server-Sent Events (SSE)를 위한 명백한 선택지는 EventSource입니다.
// ❌ 작동하지 않음
const source = new EventSource('/api/v1/chat/stream');
불행히도 여기에는 세 가지 주요 제한 사항이 있습니다:
- GET 요청만 가능
- 요청 본문 (request body)을 보낼 수 없음
- Authorization 헤더를 보낼 수 없음
Jarvis는 이 세 가지가 모두 필요합니다.
모든 채팅 요청은 다음과 같습니다:
- JWT를 통한 인증 (authenticated)
- POST 방식으로 전송
- JSON을 포함
따라서 EventSource는 단순히 선택지가 될 수 없습니다.
해결책: fetch() + ReadableStream
대신, 모든 스트리밍 엔드포인트(streaming endpoint)는 fetch()를 사용합니다.
fetch('/api/v1/chat/stream', {
method: 'POST',
headers: {
...
이 접근 방식은 다음을 지원합니다:
- JWT 인증 (authentication)
- POST 요청
- 요청 본문 (request bodies)
- 스트리밍 응답 (streaming responses)
정확히 동일한 구현이 다음 기능들을 구동합니다:
- 채팅 (Chat)
- 에이전트 (Agents)
- 음성 (Voice)
하나의 스트리밍 구현으로.
세 가지 기능을 구현합니다.
"done" 이벤트가 두 번 발생함
백엔드는 다음과 같이 이벤트를 방출합니다:
event: token
data: {"t":"Hello"}
...
프론트엔드는 done 이벤트를 올바르게 처리했습니다.
하지만 그 이후에 이상한 일이 발생했습니다.
브라우저가 스트림 읽기를 마쳤을 때, reader.read() 또한 다음과 같이 반환했습니다:
done === true
그 결과 onDone()이 두 번 실행되었습니다.
눈에 보이는 결과는 무엇이었을까요?
모든 AI 응답이 빈 어시스턴트 메시지로 끝나는 현상이 나타났습니다.
수정 방법
단 하나의 가드(guard)가 이를 해결했습니다.
let finished = false;
const finish = () => {
...
이제 두 코드 경로 모두 finish()를 호출합니다.
스트림이 어떻게 종료되든, onDone()은 정확히 한 번만 실행됩니다.
테마 시스템 설계
다크 모드(Dark mode)는 나중에 추가된 것이 아닙니다.
처음부터 설계되었습니다.
모든 색상은 CSS 사용자 정의 속성 (CSS custom property)입니다.
:root {
--bg-primary: #0f1117;
--bg-secondary: #1a1d27;
...
라이트 모드(Light mode)는 단순히 변수들을 재정의(override)합니다.
.light-theme {
--bg-primary: white;
--bg-secondary: #f8f9fc;
...
컴포넌트들은 색상을 하드코딩(hardcode)하지 않습니다.
대신 다음과 같이 사용합니다:
컴포넌트들은 색상을 하드코딩(hardcode)하지 않습니다.
대신 다음과 같이 사용합니다:
background: var(--bg-primary);
color: var(--text-primary);
이점은 엄청납니다. 테마를 전환해도 단 하나의 컴포넌트도 수정할 필요 없이 전체 애플리케이션의 색상이 다시 그려집니다(repaints).
Angular Signals Everywhere
모든 페이지는 UI 상태에 Signals를 사용합니다.
readonly messages = signal<Message[]>([]);
readonly streamingContent = signal('');
readonly isStreaming = signal(false);
계산된 상태(Computed state)는 간단하게 유지됩니다.
readonly canSend = computed(() =>
this.input().trim().length > 0 &&
!this.isStreaming()
...
우리의 규칙은 다음과 같아졌습니다:
Signals
↓
템플릿이 표시하는 모든 것
...
이러한 분리는 컴포넌트를 놀라울 정도로 작게 유지시켜 줍니다.
Stop 버튼 버그
스트리밍(streaming) 중에 Send 버튼은 Stop 버튼으로 바뀝니다.
원래 구현에서는 하나의 버튼을 재사용했습니다.
<button [disabled]=
<div role="status"
aria-live="polite">```
에러(Errors)는 다음을 사용합니다:
이제 스크린 리더(Screen readers)가 메시지를 자동으로 안내합니다.
숨겨진 삭제 버튼 (Hidden Delete Button)
불투명도(Opacity) 조절만으로는 충분하지 않습니다.
키보드 사용자가 보이지 않는 버튼으로 탭(tab) 이동을 할 수 있기 때문입니다.
해결책:
.memory-card:focus-within .memory-card__delete {
opacity: 1;
}
...
이제 키보드 내비게이션(Keyboard navigation)이 올바르게 동작합니다.
확인 대화상자 (Confirmation Dialog)
대화상자(Dialog)에 적절한 의미론적 구조(Semantics)가 추가되었습니다.
<div
role="dialog"
aria-modal="true"
...
작은 변화이지만,
접근성(Accessibility)은 훨씬 좋아졌습니다.
배운 점 (Lessons Learned)
EventSource만으로는 부족하다
만약 API가 다음을 요구한다면:
- JWT
- POST
- JSON
처음부터 fetch()와 ReadableStream을 사용하세요.
테마 토큰(Theme Tokens)의 확장성
하드코딩된 색상은 결국 다크 모드(Dark mode)를 망가뜨립니다.
모든 재사용 가능한 색상을 디자인 토큰(Design token)으로 만들었습니다.
이 결정은 새로운 페이지가 추가될 때마다 지속적인 이득을 주고 있습니다.
Signals가 템플릿을 단순화한다
시그널(Signals)은 다음을 제거합니다:
- 수동 변경 감지 (Manual change detection)
- 템플릿 구독 (Template subscriptions)
- async 파이프 체인 (Async pipe chains)
템플릿을 훨씬 읽기 쉽게 만들어 줍니다.
자동 리뷰가 실제 버그를 잡아낸다
자동 리뷰 과정에서 몇 가지 프로덕션 품질의 버그가 발견되었습니다:
onDone()의 중복 실행- 접근 불가능한 삭제 버튼
- 비활성화된 Stop 버튼
- 잘못된 CORS 설정
이러한 버그들은 사용자에게 전달되지 않았습니다.
지연 로딩(Lazy Loading)은 가치가 있었다
모든 기능이 독립적으로 로드됩니다.
{
path: 'memory',
loadComponent: () =>
...
기여자(Contributors)들은 초기 번들 크기(Initial bundle size)를 늘리지 않고도 완전히 새로운 페이지를 추가할 수 있습니다.
다음 단계 (What's Next)
세 개의 주요 페이지가 남아 있습니다:
📄 Documents
파일 업로드 (File upload)
상태 폴링 (Status polling)
...
그 이후에는 다듬기 작업이 이어집니다:
- 반응형 레이아웃 (Responsive layouts)
- 모바일 개선 (Mobile improvements)
- 엔드 투 엔드 테스트 (End-to-end testing)
- UI 정교화 (UI refinement)
백엔드는 이미 6단계에 도달했습니다.
7단계는 해당 기능을 세련된 브라우저 경험을 통해 사용할 수 있도록 만드는 것입니다.
기여하기 (Contributing)
Jarvis AI Platform은 Apache 2.0 라이선스 하에 오픈 소스로 제공됩니다.
현재 기여 가능한 이슈:
Documents Page → Good First Issue
Agents Page → Intermediate
...
GitHub:
https://github.com/sujankim/jarvis-ai-platform
Jarvis AI Platform 시리즈
- Part 1 — Spring Boot 4를 사용한 로컬 우선(Local-First) AI 비서 구축
- Part 2 — pgvector를 사용한 장기 기억(Long-Term Memory)
- Part 3 — 의미론적 메모리 검색(Semantic Memory Retrieval)
- Part 4 — Spring AI를 사용한 도구 엔진(Tool Engine)
- Part 5 — Whisper 및 Text-to-Speech를 사용한 음성 비서
- Part 6 — ReACT 패턴을 사용한 AI 에이전트 시스템 구축
- Part 7 — Angular 22를 사용한 실시간 웹 UI 구축 (본문)
당신의 AI. 당신의 데이터. 당신의 기계.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기