Streaming Long AI Jobs to the Browser: SSE Patterns From Building an Audit Tool
개요
웹 프론트엔드에서 다중 분 이상 소요되는 AI 작업을 위한 Server-Sent Events(SSE) 패턴은 사용자의 이탈을 방지하고 작업 진행 상황을 효과적으로 전달하는 데 중요한 역할을 한다.
주요 내용
* SSE의 채택 이유: 웹 브라우저가 작업 진행 중 서버와 통신할 필요가 없는 단방향 통신 구조에 SSE가 적합하며, EventSource API의 자동 재연결 기능이 장점에 해당한다. WebSockets는 양방향 통신 기능 중 일부만 사용하게 되어 비효율적이다.
* 이벤트 어휘 설계의 중요성: 작업 진행 상태, 부분 결과, 완료, 오류 등 고정된 타입의 이벤트를 미리 정의하여 API 계약처럼 관리하면 프론트엔드 개발이 용이해진다.
* 진행 단계별 결과 전달:
* Progress 이벤트: 백분율 대신 "함수 분석 중, 7단계 중 3단계"와 같이 구체적인 진행 단계를 전달하여 사용자가 지루함을 덜 느끼게 한다.
* Partial-Finding 이벤트: 검증된 부분적인 결과(Finding)를 발견 즉시 스트리밍하여 사용자가 작업이 완료되기 전에도 결과를 확인할 수 있도록 한다. 이는 실제 작업 시간은 동일하더라도 인지되는 대기 시간을 줄여준다.
* Done 이벤트: 작업 완료 시 요약 정보를 전달한다.
* Error 이벤트: 오류 발생 시 복구 가능한 오류인지 아닌지를 구분하여 사용자에게 적절한 피드백을 제공한다.
* 재연결(Reconnection) 메커니즘:
* 클라이언트가 네트워크 전환, 절전 모드 등으로 인해 연결이 끊어졌을 때, Last-Event-ID 헤더를 통해 마지막으로 받은 이벤트 ID를 서버에 전달하여 누락된 이벤트를 재전송받는다.
* 이를 위해 모든 이벤트에는 고유한 단조(monotonic) ID가 부여되어야 하며, 서버는 각 작업별 이벤트 로그를 재생 가능하도록 저장해야 한다.
* 작업은 HTTP 요청과는 별도로 영속적인 스토리지에 저장되어야 하며, SSE 엔드포인트는 해당 로그의 커서 역할을 한다.
* 하트비트(Heartbeat)와 프록시 문제: SSE 연결이 장시간 유휴 상태일 때 프록시 서버에 의해 연결이 끊어지는 것을 방지하기 위해 주기적으로 빈 줄(lines starting with :)을 전송하여 연결을 유지한다. no-transform 헤더를 사용하여 프록시의 스트림 버퍼링을 방지한다.
* Serverless 환경에서의 제약: 기본 Serverless 환경은 요청 수명과 작업 수명이 일치하는 것을 가정하므로, 다중 분 이상 소요되는 작업을 위해서는 별도의 워커 프로세스나 큐 등을 사용하여 작업의 영속성을 확보해야 한다. SSE 엔드포인트는 상태가 없는(stateless) 저비용 엔드포인트로 유지하고, 작업은 내구성 있는(durable) 곳에서 실행하며 이벤트를 공유 스토리지에 기록하는 구조가 이상적이다.
* 백프레셔(Backpressure) 처리:
* 이벤트 통합(Coalesce): 너무 빈번하게 발생하는 Progress 이벤트는 짧은 간격으로 묶어 최신 상태만 전송한다. Finding 이벤트는 통합 없이 모두 전송한다.
* 스트림 신호 존중: controller.desiredSize를 확인하여 클라이언트가 처리할 수 있는 버퍼 공간이 없을 경우, 저우선순위 이벤트(Progress)는 건너뛰어 메모리 누수를 방지한다.
* 핵심 패턴: 이벤트 로그를 제품으로 간주하고 SSE 연결을 휘발성 뷰(disposable view)로 취급함으로써 재연결, Serverless 제약, 백프레셔, 다중 탭 동시 보기 등 복잡한 문제들을 해결할 수 있다.
시사점
이벤트 로그를 중앙 소스 오브 트루스(source of truth)로 삼고 SSE 연결을 단순한 데이터 전달 채널로 분리하는 접근 방식은 장기 실행되는 브라우저 기반 작업에서 사용자 경험을 향상시키고 안정성을 확보하는 데 효과적인 아키텍처 패턴을 제공한다.
댓글
GitHub Discussions