WebSockets와 Cohere Command-A를 활용한 실시간 협업 상태 시트 구축
요약
WebSockets와 Cohere Command-A를 활용하여 서버 비용을 최소화하면서 실시간 협업이 가능한 상태 시트 아키텍처를 구축하는 방법을 설명합니다. 'POST-once' 모델을 통해 데이터 저장 및 AI 호출 빈도를 획기적으로 줄이는 설계 전략을 다룹니다.
핵심 포인트
- WebSockets 기반의 'POST-once' 모델로 서버 비용 절감
- 도메인 레벨 캐싱을 통한 데이터 중복 저장 방지
- AI 모델 호출을 주 1회로 제한하는 조건부 지속성 구현
- 원시 WebSocket 스트림을 권위 있는 기록 원천으로 활용
조건부 지속성(Conditional persistence), 낙관적 업데이트(optimistic updates), 그리고 일주일에 단 한 번만 AI를 호출하는 이유
작성자: Kiprono Ngetich
우리는 실시간 협업을 원했습니다. AI가 생성하는 상태 추출(status extraction) 기능도 원했습니다. 그리고 50명이 동일한 시트를 열었을 때 서버 비용이 폭발하지 않기를 원했습니다.
모든 것을 저장하고 모든 뷰(view)마다 재계산하는 표준적인 접근 방식은 고려 대상이 아니었습니다.
그래서 우리는 다른 방식을 구축했습니다. REST API는 한 번만 쓰는 초기화 저장소(write-once initialization store) 역할을 하고, WebSockets가 그 외의 모든 것을 처리하는 'POST-once' 지속성 모델입니다.
이것이 어떻게 작동하는지, 추출을 위해 왜 Cohere Command-A를 선택했는지, 그리고 그 과정에서 무엇이 망가졌는지 설명하겠습니다.
1. 핵심 문제
상태 시트(Status sheets)는 쓰기(write) 단계에서 무너집니다.
전통적인 프로젝트 관리 도구(Jira, Asana, Monday.com)는 주로 업무 수행에 집중하는 기여자들에게 절제된 데이터 입력을 요구합니다. 그 결과, 시스템은 활발하게 유지 관리될 때만 정확하며, 이는 업무 여력이 가장 부족할 가능성이 높은 사람들에게 부차적인 작업 부담을 주는 결과를 초래합니다.
우리의 목표는 이를 뒤집는 것이었습니다. 상태 시트를 추가적인 업무가 아닌, 정상적인 업무의 부산물로 만드는 것입니다.
2. WeekData 추상화
우리 시스템의 중심 데이터 구조는 개별 작업 주(working week)와 관련된 모든 지능을 캡슐화하는 WeekData 객체입니다.
WeekData 인스턴스는 다음을 포함합니다:
weekNumber (ISO 주 식별자)
...
핵심 설계 선택: WeekData는 단 하나의 원시 메시지(raw message)로부터 구축됩니다. 이는 플랫폼이 저장된 단 하나의 메시지로부터 전체 주간 지능을 재구성할 수 있음을 의미하며, 원시 WebSocket 스트림을 권위 있는 기록 원천(authoritative source of record)으로 만듭니다.
3. POST-once 모델
이것이 가장 중요한 아키텍처 결정입니다.
AI 모델을 호출하기 전에, 시스템은 지속성 API(persistence API)를 대상으로 GET 조회를 수행합니다. 생성 작업 — 그리고 결과 저장을 위한 후속 POST 작업 — 은 현재 주에 대한 기존 데이터가 조회되지 않을 때만 발생합니다.
그 영향은 다음과 같습니다:
상태 시트를 여는 사용자 수와 관계없이 서버 저장소는 최대 주당 1개의 레코드 속도로 증가합니다.
페이지 새로고침 빈도와 관계없이 AI 모델은 최대 주당 1회 호출됩니다.
모든 후속 변경 사항은 REST API 쓰기 작업 없이 오직 WebSocket을 통해서만 전파됩니다.
이 조건부 영속성 (conditional persistence) 모델은 돌이켜보면 당연해 보이지만, 시작 단계에서는 당연하지 않았습니다. 우리의 첫 번째 설계는 모든 사용자의 뷰 상태 (view state)를 별도로 저장했습니다. 동일한 주의 상태 시트를 보고 있는 50명의 팀원의 경우, 우리는 동일한 데이터의 50개 복사본을 저장하고 있었습니다.
현재의 설계는 저장 비용을 두 자릿수(two orders of magnitude)만큼 절감했습니다.
교훈: 뷰 레벨 (view level)이 아닌 도메인 레벨 (domain level)에서 캐싱하십시오.
text
# 생성 트리거 로직 (단순화됨)
async def get_or_generate_status_sheet(week_id):
...
4. AI 파이프라인: 신뢰성을 위한 프롬프트 엔지니어링 (prompt engineering)
우리는 강력한 지시 이행 (instruction-following) 특성과 구조화된 출력 (structured output)의 신뢰성을 고려하여 Cohere의 Command-A 모델 (command-a-03-2025)을 사용합니다.
생성 프롬프트는 두 부분으로 구성됩니다:
시스템 프롬프트 (System prompt): 출력 스키마 (정확히 9개의 열을 가진 마크다운 테이블)와 품질 제약 조건 (10-20 단어의 활동 설명, 특정 댓글 형식)을 정의합니다. 모델은 테이블 구조 외에 설명용 산문 (explanatory prose)을 추가하는 것이 명시적으로 금지됩니다.
사용자 프롬프트 (User prompt): WeekData의 작업 목록으로부터 구성되며, 각 작업의 제목, 요약 (brief), 마감일, 담당자, 그리고 전체 상세 텍و스트를 드러내는 구조화된 텍스트로 직렬화됩니다.
모델의 마크다운 출력은 파이프 구분자 분할 (pipe-delimited split) 및 헤더 감지를 통해 한 줄씩 파싱됩니다. 최소 9개 열을 충족하지 못하는 행은 조용히 삭제됩니다.
모델의 거부, 형식 이탈, 또는 네트워크 실패로 인해 파싱된 결과가 0개의 행을 생성할 경우, 시스템은 AI 보강 없이 WeekTask 목록에서 직접 행을 구성하는 방식으로 폴백 (fallback)합니다. 이러한 우아한 성능 저하 (graceful degradation)를 통해 상태 시트가 항상 사용 가능한 상태를 보장합니다.
5. WebSocket 프로토콜: 다섯 가지 메시지 유형
협업 프로토콜은 세 가지 원칙을 기반으로 구축되었습니다:
-
작업 기반 메시징 (Operation-based messaging: 전체 상태가 아닌 변경된 사항만 전송)
-
사람이 읽을 수 있는 형식 (Human-readable formats: 파서(Parser) 없이도 로그를 남길 수 있는 파이프(|) 구분 문자열)
-
낙관적 로컬 적용 (Optimistic local application: 서버의 확인이 있기 전에 변경 사항을 로컬에 먼저 적용)
다섯 가지 메시지 유형은 다음과 같습니다:
메시지 유형 형식
CELL_EDIT CELL_EDIT|rowIndex|column|oldValue|newValue|username
BATCH_UPDATE BATCH_UPDATE|rowIndex|column|value|username
...
CELL_EDIT는 향후 충돌 감지(Conflict detection) 및 실행 취소(Undo) 기능을 가능하게 하기 위해 이전 값(oldValue)과 새 값(newValue)을 모두 전달합니다. ROW_ADD는 다중 필드 객체를 전송할 때 타입 충실도(Type fidelity)를 보장하기 위해 JSON으로 직렬화된 StatusRow를 전달합니다.
사용자 존재 여부(User presence)는 클라이언트 측에서 일시적인 집합(Ephemeral set)으로 유지됩니다. 존재 여부는 실시간 지표이며, 과거 기록이 아닙니다. 재연결된 클라이언트는 자신을 다시 알림으로써, 연결이 끊어진 후에도 존재 여부 집합이 스스로 복구(Self-heals)되도록 보장합니다.
6. 무엇이 고장 났는가 (솔직한 실패 사례)
운영 환경에서 세 가지 문제가 발생했습니다:
-
모델 거부 (Model refusal): 가끔 모델이 테이블 대신 "이 요청을 완료할 수 없습니다"라는 출력을 내보냈습니다. 우리의
_createFallbackRows()폴백(Fallback) 방식이 이를 처리했지만, 프롬프트를 수정(날짜 참조 제거)하여 재시도 로직(Retry logic)을 추가함으로써 거부율을 약 80% 감소시켰습니다. -
포맷팅 드리프트 (Formatting drift): 모델이 때때로 추가 열을 생성하거나 셀을 병합했습니다. 우리는 파서(Parser)를 허용적으로 만들었습니다. 즉, 파서는 추가된 열을 버리고, 누락된 열은 전체 행을 실패 처리하는 대신 빈 문자열로 취급합니다.
-
재연결 폭풍 (Reconnection storms): WebSocket 서버가 재시작될 때, 모든 클라이언트가 동시에 재연결을 시도하며 전체 상태 시트를 다시 요청했습니다. 우리는 부하를 분산하기 위해 지터가 포함된 백오프(Jittered backoff, 3초 + 0~2초 무작위)를 추가했습니다.
7. 우리가 다르게 했을 일
가장 크게 누락된 부분은 양방향 피드백 루프(Two-way feedback loop)입니다.
현재 상태 시트에 대한 사람의 편집 사항은 프로젝트 상태에 대한 AI의 이해로 다시 흘러 들어가지 않습니다. 프로젝트 매니저가 마감일을 수정하더라도, AI는 자신이 틀렸다는 사실을 학습하지 못합니다.
우리는 이를 향해 나아가고 있습니다. 각 StatusRow (상태 행)에 있는 isManuallyEdited 플래그가 그 기초가 됩니다. 이 플래그는 어떤 필드가 인간의 판단을 나타내는지, 아니면 AI 추출 (AI extraction) 결과인지를 알려줍니다. 다음 단계는 수정된 행들을 향후 주차를 위한 프롬프트 컨텍스트 (prompt context)로 다시 피딩 (feeding)하는 것입니다.
로드맵에 있는 다른 개선 사항들:
- 동시 셀 편집을 위한 운영 변환 (Operational transforms) 또는 CRDT (Conflict-free Replicated Data Types)
- 셀별 감사 추적 (Per-cell audit trails) (사용자 이름 + 타임스탬프)
- PDF 및 XLSX로 내보내기
결론
이 시스템이 완벽하지는 않습니다. AI는 때때로 문맥 (context)을 놓칩니다. WebSocket 프로토콜은 아직 운영 변환 (operational transforms)을 통한 동시 셀 편집을 처리하지 못합니다.
하지만 핵심 패턴인 조건부 지속성 (conditional persistence), 작업 기반 메시징 (operation-based messaging), 우아한 AI 성능 저하 (graceful AI degradation)는 수개월간의 내부 사용을 통해 신뢰할 수 있음이 증명되었습니다.
만약 여러분이 협업 중심의 AI 보조 문서 도구를 구축하고 있다면, 'POST-once' 패턴을 가져다 쓰십시오. 이것이 데모와 실제 배포 (deployment)를 가르는 차이점입니다.
Kiprono Ngetich는 AI와 협업의 접점에서 소프트웨어를 구축합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기