Google Gemini 기반 AI 페르소나 및 실시간 사용자 채팅 플랫폼
생성형 AI의 특성상 긴 답변을 생성할 때 수 초 이상의 지연 시간가 발생했습니다. HTTP 요청-응답 방식으로는 AI가 답변을 완료할 때까지 사용자가 기다려야 했고, 이는 대화의 몰입감을 해치는 치명적인 UX 저하로 이어졌습니다.
Gemini SDK의 스트림 기능과 Socket.io를 결합한 파이프라인을 구축하여 해결했습니다.
sendMessageStream을 호출하여 청크 단위로 데이터를 수신합니다.ai-stream 이벤트로 클라이언트에 전달합니다.ai-stream 이벤트를 구독하여 실시간으로 답변이 타이핑되는 듯한 효과를 구현했습니다.for await (const chunk of stream) { if (chunk) { emitter.emit('ai-stream', { text: chunk.text }); } } emitter.emit('ai-stream-done', { fullText });
HTTP API와 달리 WebSocket 연결은 표준 헤더 인증 방식이 모호했습니다. 단순히 소켓 연결 후 토큰을 보내는 방식은 보안상 취약할 수 있고, 연결 시점에 즉시 유저를 특정하여 채팅방 접근 권한을 제어해야 했습니다.
Socket.io의 Handshake 과정을 인터셉트하여 인증 로직을 구현했습니다.
handleConnection 단계에서 쿠키를 파싱하고 세션을 검증합니다.data 속성에 주입하여, 이후 발생하는 모든 이벤트에서 별도 인증 없이 유저를 식별하도록 최적화했습니다.async handleConnection(@ConnectedSocket() socket: Socket) { const cookieHeader = socket.handshake.headers.cookie; try { const user = await this.getCurrentUser(cookieHeader); socket.data.user = user; } catch (error) { socket.disconnect(); } }
React Suspense 도입 후 비동기 로직이 있는 컴포넌트에 Suspense 래퍼를 누락하여 발생하는 런타임 에러가 빈번했습니다. 컴포넌트 깊이가 깊어질수록 육안으로 계층 구조를 파악하기 어려워져 휴먼 에러로 인한 서비스 장애가 발생했습니다.
정적 분석으로는 어떤 타이밍에 Promise를 throw하는지 알기 어려워 Eslint 커스텀 룰을 제작하여 네이밍 컨벤션을 강제하는 전략을 선택했습니다. 만약 useSuspenseQuery 같은 훅을 내부에서 사용한다면 그 커스텀 훅과 컴포넌트의 이름도 반드시 Suspense로 시작해야 한다는 컨벤션을 강제합니다.
// ❌ Error function useUser() { return useSuspenseQuery(...); } // ✅ Pass function useSuspenseUser() { return useSuspenseQuery(...); }
프론트엔드와 백엔드가 User, Message 등 동일한 타입 정의를 중복해서 관리하다 보니, 스키마 변경 시 양쪽을 모두 수정해야 하는 비효율이 발생했습니다.
Yarn Workspace를 활용한 모노레포 환경을 구축했습니다.
채팅방 진입 시마다 DB에서 전체 대화 내역을 매번 조회하다 보니, 트래픽이 몰릴 경우 DB 부하가 급증하고 응답 속도가 평균 5.7ms까지 지연되는 병목이 발생했습니다.
인메모리 DB인 Redis를 활용한 Look-aside 캐싱 전략을 도입했습니다.
// chat.service.ts (Backend) async getChatHistory(roomId: string) { // 1. 캐시 조회 (메모리 I/O) const cached = await this.redis.get(`chat:${roomId}`); if (cached) return JSON.parse(cached); // 2. DB 조회 (Disk I/O) const history = await this.chatRepo.find({ where: { roomId } }); // 3. 캐싱 (TTL 설정으로 메모리 관리) await this.redis.set(`chat:${roomId}`, JSON.stringify(history), 'EX', 3600); return history; }