HUNIL PARK

Joshua AI Agent

KoGPT-2 기반 AI 카피라이팅 에이전트

ElectronAngularFastAPIPostgreSQLKoGPT-2Stripe
Joshua AI Agent

프로젝트 개요

Joshua AI Agent는 KoGPT-2 언어모델을 기반으로 한 AI 카피라이팅 서비스입니다. 마케팅 콘텐츠와 광고 카피를 생성하는 유료 구독형 데스크톱 애플리케이션으로, Windows와 macOS를 모두 지원하는 크로스플랫폼 환경 구축이 필수였습니다. FastAPI 기반 백엔드와 PostgreSQL 데이터베이스를 활용한 AI 모델 서빙 아키텍처에서 프론트엔드를 담당했습니다. 주된 기술적 과제는 Electron 환경에서 Angular 프레임워크를 통합하고, AI 응답의 실시간 스트리밍 렌더링을 구현하며, Stripe 결제 플로우를 데스크톱 앱 환경에 적합하게 구성하는 것이었습니다.

프로젝트에서 프론트엔드 개발 전반을 담당했으며, Electron과 Angular를 결합한 데스크톱 애플리케이션 아키텍처 설계부터 AI 응답 인터페이스 구현, Stripe 결제 연동까지 수행했습니다. 특히 Electron의 Main 프로세스와 Renderer 프로세스 간 IPC(Inter-Process Communication) 통신 구조를 설계하여 백엔드 API와의 통신 로직을 Main 프로세스에서 처리하고, Angular 기반 Renderer 프로세스에서는 순수하게 UI 렌더링과 사용자 인터랙션에 집중할 수 있도록 관심사를 분리했습니다. AI 생성 콘텐츠의 점진적 스트리밍 표시를 위해 RxJS를 활용한 비동기 데이터 처리 파이프라인을 구축했으며, Stripe의 Checkout 세션 기반 결제 플로우를 데스크톱 환경에 맞게 커스터마이징했습니다.

Joshua AI Agent architecture

기술 구현

Electron + Angular 크로스플랫폼 데스크톱 앱 아키텍처

일반적으로 Electron은 React나 Vue와 결합하는 경우가 많고, Angular와의 통합 사례가 상대적으로 적어 레퍼런스가 부족했습니다. Angular의 복잡한 빌드 설정(Webpack, TypeScript 컴파일러)과 Electron의 Main/Renderer 프로세스 구조를 조화롭게 구성해야 했고, 개발 환경과 프로덕션 빌드 환경 모두에서 안정적으로 작동하는 빌드 파이프라인이 필요했습니다. 특히 Angular의 개발 서버를 Electron 프로세스와 연동하는 HMR(Hot Module Replacement) 환경 구성이 핵심 과제였습니다.

Electron Builder를 활용해 Angular 애플리케이션을 Electron 환경에 패키징하는 빌드 워크플로우를 구축했습니다. Main 프로세스는 순수 TypeScript로 작성하여 BrowserWindow 생성, IPC 핸들러 등록, 백엔드 API 통신 등 네이티브 기능과 시스템 레벨 로직을 담당하게 했습니다. Renderer 프로세스는 Angular CLI 기반으로 빌드된 결과물을 로드하도록 구성했으며, 개발 모드에서는 Angular dev server(localhost:4200)를 Electron이 로드하고, 프로덕션 빌드에서는 file:// 프로토콜로 dist 폴더의 정적 파일을 로드하는 방식으로 환경을 분리했습니다. IPC 통신은 ipcRenderer와 ipcMain을 사용한 request-response 패턴으로 설계해, Renderer에서 사용자 입력을 받으면 Main을 통해 백엔드 API를 호출하고, 응답을 다시 Renderer로 전달하는 단방향 데이터 흐름을 유지했습니다.

Windows와 macOS 양쪽에서 안정적으로 동작하는 크로스플랫폼 데스크톱 앱을 출시할 수 있었습니다. 개발 환경에서는 Angular의 HMR을 활용해 빠른 개발 사이클을 유지하면서도, 프로덕션 빌드에서는 단일 실행 파일(Windows: .exe, macOS: .dmg)로 배포할 수 있는 구조를 확립했습니다. IPC 통신 레이어 덕분에 백엔드 API 변경사항이 있어도 Main 프로세스만 수정하면 되어, Angular 컴포넌트 코드와의 결합도를 낮게 유지할 수 있었습니다.

AI 응답 실시간 스트리밍 렌더링 인터페이스

KoGPT-2 모델이 생성하는 텍스트를 사용자에게 보여주는 방식이 중요했습니다. 긴 카피 문구를 한 번에 표시하면 사용자가 대기 시간 동안 아무런 피드백을 받지 못해 UX가 저하되고, 반대로 스트리밍 방식으로 토큰 단위로 점진적으로 표시하면 '생성 중'이라는 느낌을 주어 체감 속도가 빨라집니다. 백엔드에서 Server-Sent Events(SSE) 또는 청크 방식으로 응답을 보내주면, 이를 프론트엔드에서 실시간으로 받아 화면에 렌더링해야 했습니다. Angular 환경에서 비동기 스트림 데이터를 효율적으로 처리하면서 UI를 부드럽게 업데이트하는 방법이 필요했습니다.

RxJS의 Observable 기반 비동기 처리 파이프라인을 구성했습니다. Electron IPC를 통해 Main 프로세스에서 백엔드 API의 스트리밍 응답을 받으면, 이를 청크 단위로 Renderer 프로세스에 이벤트로 전달하고, Angular 컴포넌트에서 Observable로 구독하여 화면에 반영했습니다. 구체적으로는 백엔드 응답을 fetch API의 ReadableStream으로 읽어들이고, 각 청크를 파싱하여 ipcRenderer.send로 Renderer에 전송하는 방식을 채택했습니다. Angular 컴포넌트에서는 fromEvent로 IPC 이벤트를 Observable화하여 scan 오퍼레이터로 누적 텍스트를 생성하고, async pipe를 통해 템플릿에 바인딩했습니다. 이를 통해 텍스트가 타이핑되는 듯한 자연스러운 애니메이션 효과를 구현했습니다.

사용자는 AI가 카피를 생성하는 과정을 실시간으로 볼 수 있어 대기 시간이 체감상 짧아졌고, 앱 사용 경험이 개선되었습니다. RxJS 기반 아키텍처 덕분에 에러 핸들링(catchError), 타임아웃(timeout), 재시도 로직(retry)을 선언적으로 추가할 수 있었고, 백엔드 API가 불안정해도 프론트엔드에서 안정적으로 대응할 수 있었습니다. 사용자 피드백에서도 '생성 과정이 보여서 좋다'는 긍정적인 반응을 얻었습니다.

Stripe 결제 연동 및 구독 관리 UI

유료 구독 모델이었기 때문에 Stripe Checkout을 통한 결제 플로우와 구독 상태 관리 UI가 필수였습니다. 일반적인 웹 앱과 달리 데스크톱 앱 환경에서는 브라우저를 별도로 열어 결제를 진행하고, 결제 완료 후 다시 앱으로 돌아와야 하는 흐름을 구성해야 했습니다. Stripe Checkout Session을 생성하고, 외부 브라우저에서 결제를 완료한 뒤, 앱이 결제 완료 상태를 감지하여 UI를 업데이트하는 로직이 필요했습니다. 또한 사용자가 현재 구독 중인지, 무료 체험 기간인지, 결제가 실패했는지를 명확하게 보여주는 대시보드 인터페이스도 구현해야 했습니다.

Stripe Checkout의 웹 기반 결제 페이지를 Electron의 shell.openExternal API를 통해 기본 브라우저에서 열도록 구성했습니다. 백엔드에서 Stripe Checkout Session을 생성하면 클라이언트는 해당 URL을 받아 브라우저를 실행하고, 사용자는 Stripe의 안전한 결제 페이지에서 카드 정보를 입력합니다. 결제 완료 후 Stripe Webhook을 통해 백엔드가 사용자 구독 상태를 업데이트하면, 앱은 주기적으로(또는 사용자가 '결제 완료' 버튼을 누르면) 백엔드 API를 polling하여 최신 구독 상태를 가져와 UI를 갱신하는 방식으로 구현했습니다. Angular 서비스 레이어에서 구독 정보를 BehaviorSubject로 관리하여 앱 전역에서 실시간으로 구독 상태를 참조할 수 있게 했고, 구독 만료일, 다음 결제일 등의 정보를 대시보드에 표시했습니다.

데스크톱 앱 환경에서도 안전하고 표준적인 Stripe 결제 플로우를 제공할 수 있었고, 사용자는 앱 내에서 결제 버튼을 누르면 브라우저가 열려 결제를 완료한 뒤 다시 앱으로 돌아오는 자연스러운 경험을 제공받았습니다. 구독 상태 관리 UI 덕분에 사용자는 현재 자신의 구독 플랜과 남은 기간을 명확히 인지할 수 있었고, 고객 지원 문의도 크게 줄었습니다. Stripe Webhook과 polling 방식의 조합으로 결제 완료 이벤트를 놓치지 않고 안정적으로 처리할 수 있었습니다.

트러블슈팅

Electron IPC 통신 시 대용량 데이터 전송 오류

AI가 생성한 긴 텍스트나 다량의 히스토리 데이터를 Main 프로세스에서 Renderer 프로세스로 IPC를 통해 전달할 때 간헐적으로 오류가 발생했습니다. 특정 크기 이상의 데이터를 전송하면 Electron이 메모리 부족 에러를 발생시키거나, IPC 채널이 블로킹되어 앱이 멈추는 현상이 관찰되었습니다. 초기에는 ipcRenderer.send와 ipcMain.on을 사용해 모든 데이터를 한 번에 전송하려 했으나, 데이터가 클 경우 직렬화 과정에서 병목이 발생했습니다.

대용량 데이터는 청크 단위로 분할하여 여러 번 전송하는 스트리밍 방식으로 변경했습니다. ipcRenderer.invoke와 ipcMain.handle을 사용하는 Promise 기반 IPC 패턴으로 전환하고, 데이터를 일정 크기(예: 1MB) 단위로 나누어 순차적으로 전송한 뒤 Renderer에서 조립하는 로직을 구현했습니다. 또한 불필요한 데이터는 전송하지 않도록 백엔드 응답 구조를 최적화하고, 프론트엔드에서는 필요한 필드만 선택적으로 전달받는 GraphQL 스타일의 쿼리 접근 방식을 모방했습니다.

대용량 데이터 전송 시에도 안정적으로 동작하게 되었고, IPC 통신 병목 현상이 해소되어 앱 응답성이 크게 개선되었습니다. 사용자가 긴 히스토리를 조회하거나 다량의 카피를 일괄 생성해도 앱이 멈추지 않고 부드럽게 처리할 수 있었습니다.

Angular Zone.js와 Electron 이벤트 루프 충돌

Angular는 Zone.js를 사용해 변경 감지를 자동화하는데, Electron의 IPC 이벤트가 Angular Zone 외부에서 발생하면 UI가 자동으로 업데이트되지 않는 문제가 있었습니다. 특히 ipcRenderer.on으로 등록한 이벤트 리스너에서 컴포넌트 상태를 변경해도 화면에 반영되지 않아, 사용자가 수동으로 다른 화면으로 이동했다가 돌아와야 업데이트가 보이는 상황이 발생했습니다. Zone.js의 동작 원리를 이해하지 못한 채 개발하면서 발생한 문제였습니다.

NgZone 서비스를 주입하여 IPC 이벤트 핸들러 내에서 zone.run()을 명시적으로 호출해 Angular의 변경 감지 사이클에 진입하도록 수정했습니다. 모든 IPC 이벤트 수신 로직을 Angular 서비스로 캡슐화하고, fromEvent로 Observable화한 뒤 observeOn(asyncScheduler)를 사용해 Angular Zone 내에서 실행되도록 보장했습니다. 이를 통해 IPC 이벤트가 발생하면 자동으로 UI가 업데이트되도록 만들었습니다.

IPC 이벤트 기반 상태 변경이 즉시 화면에 반영되어 사용자 경험이 크게 개선되었습니다. Zone.js의 동작 원리를 깊이 이해하게 되었고, Electron + Angular 환경에서의 이벤트 처리 베스트 프랙티스를 확립할 수 있었습니다.

회고

Electron과 Angular를 결합한 크로스플랫폼 데스크톱 애플리케이션 개발 경험을 쌓으면서, 웹 기술 스택으로 네이티브 앱을 구축하는 아키텍처에 대한 이해가 깊어졌습니다. IPC 통신 구조 설계, Main/Renderer 프로세스의 역할 분리, Angular Zone.js와 Electron 이벤트 루프의 상호작용 등 웹 프레임워크와 데스크톱 환경을 결합할 때 발생하는 특수한 문제들을 해결하며 많은 것을 배웠습니다. RxJS를 활용한 비동기 데이터 스트리밍 처리 경험은 이후 다른 프로젝트에서도 유용하게 적용할 수 있었고, Stripe 결제 통합을 통해 SaaS 비즈니스 모델의 프론트엔드 구현 역량도 확보했습니다. 특히 AI 모델 응답을 실시간 스트리밍으로 렌더링하는 인터페이스를 구현하며, 단순히 데이터를 보여주는 것을 넘어 사용자에게 '기다림'을 '경험'으로 전환하는 UX 설계의 중요성을 체감했습니다.

프로젝트 초기에 Electron과 Angular의 통합 방식에 대한 레퍼런스가 부족해 많은 시행착오를 겪었습니다. 미리 충분한 기술 검증(POC)을 수행하고 아키텍처를 확정했다면 개발 속도를 높일 수 있었을 것입니다. 또한 IPC 통신 레이어에 대한 테스트 코드가 부족해 디버깅에 많은 시간을 소비했는데, Main 프로세스와 Renderer 프로세스 간 통신 로직에 대한 단위 테스트와 통합 테스트를 체계적으로 작성했다면 안정성을 더 높일 수 있었을 것입니다. 성능 최적화 측면에서도 번들 사이즈 최적화(Tree-shaking, Lazy Loading)와 메모리 프로파일링을 더 철저히 수행했다면 앱 실행 속도와 리소스 사용량을 개선할 수 있었을 것으로 생각합니다. 향후 유사한 프로젝트에서는 초기 설계 단계에서 테스트 전략과 성능 목표를 명확히 수립하고, 지속적인 모니터링과 개선을 병행할 계획입니다.