본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 21. 12:31

AI 에이전트가 Yjs 룸에 참여할 때 깨지는 세 가지 가정

요약

AI 에이전트를 Yjs 협업 스택의 서버 측 피어로 통합할 때 발생하는 아키텍처적 문제를 다룹니다. 인간의 속도를 전제로 설계된 기존 CRDT 모델이 AI의 압도적인 처리량과 상호작용 방식에 어떻게 대응해야 하는지 분석합니다.

핵심 포인트

  • AI 에이전트를 서버 측 Yjs 피어로 연결하는 아키텍처의 타당성 제시
  • 인간의 연산 속도를 전제로 한 기존 CRDT 설계의 한계 지적
  • 백프레셔, 실행 취소 소유권, 존재감 주기 측면에서의 가정 붕괴 설명
  • AI 에이전트의 높은 처리량이 협업 시스템에 미치는 영향 분석

LLM을 일급 Yjs 피어(peer)로 연결하는 것은 아키텍처적으로 타당합니다. 하지만 이는 여러분의 협업 스택이 피어 대칭성(peer symmetry)에 대해 이미 전제하고 있는 세 가지 암묵적인 가정, 즉 처리량(throughput), 실행 취소 소유권(undo ownership), 그리고 존재감 주기(presence cadence)를 무효화합니다.

여러분은 실제 협업 부하 상황에서 Yjs 프로바이더(provider)를 튜닝해 본 경험이 있습니다. 이름을 붙이기 전에도 그 느낌을 알고 있을 것입니다. 하나의 무거운 클라이언트가 룸(room)을 느리게 만들기 시작하고, 존재감 업데이트(presence updates)가 끊기며, 결국 어딘가에 디바운스(debounce)를 추가하고 상황이 해결되었다고 생각하는 바로 그 느낌 말입니다.

이제 그 클라이언트가 분당 3,000단어의 속도로 텍스트를 생성하고, 절대 오프라인이 되지 않으며, 자체적인 어웨어니스 커서(awareness cursor)를 가지고 있다고 상상해 보십시오.

이것은 단순한 사이드바 기능이 아닙니다. 이것은 새로운 클래스의 피어이며, 여러분의 협업 아키텍처는 이를 위해 설계되지 않았습니다.

데모는 실제이지만, 어려운 부분은 건너뜁니다

2026년 4월, 작동하는 데모가 LLM을 진정한 서버 측 Yjs 문서 피어로 연결했습니다. 인간 편집자와 동일한 전송 방식(transport), 동일한 CRDT, 그리고 자체적인 어웨어니스 상태(awareness state)를 사용합니다. 이 구현은 y-prosemirror와 표준 어웨어니스 프로토콜(awareness protocol)을 직접 사용합니다. 만약 여러분이 TipTap 협업 기능을 출시했다면, 이미 필요한 모든 의존성(dependency)을 갖추고 있는 셈입니다.

이 아키텍처는 올바릅니다. 에이전트를 REST 엔드포인트를 통해 차이점(diffs)을 게시하는 클라이언트 측 부가 기능으로 만드는 대신, 서버 측 피어로 만드는 것은 두 개의 모델 대신 하나의 수렴 모델(convergence model)을 제공하며, 에이전트를 위한 실제 존재감 의미론(presence semantics)을 제공하고, LLM 스트리밍 레이어와 문서 상태 레이어 사이의 깔끔한 분리를 가능하게 합니다.

하지만 이 데모는 피어 모델을 확립할 뿐입니다. 해당 피어가 실행될 때 여러분의 기존 가정들에 어떤 일이 발생하는지에 대한 스트레스 테스트(stress-test)는 수행하지 않습니다.

모든 CRDT 구현이 갖는 암묵적인 가정

여기에 그것이 있습니다. Yjs 어웨어니스 프로토콜, 언두 매니저(undo manager), 그리고 여러분의 백프레셔(backpressure) 전략에 내재된 가정이며, 지금까지는 항상 사실이었기에 아무도 기록하지 않았던 가정입니다:

모든 피어는 대략 인간의 속도로 연산(operations)을 생성한다.

속도가 동일하지는 않습니다. 인간 타이피스트마다 차이가 있죠. 하지만 그들은 동일한 차수(order of magnitude) 내에 머뭅니다. 인지 상태(awareness)를 얼마나 자주 브로드캐스트할지, 실행 취소 기록(undo history)의 범위를 어떻게 설정할지, 애플리케이션 계층에서 피어별 속도 제한(rate limiting)이 필요한지 등 전체 설계 공간(design space)은 바로 그 암묵적인 계약에 기반하고 있습니다.

분당 1,0004,000단어를 작성하는 AI 에이전트는 이 범위를 25100배 벗어납니다. 이는 단순히 전송 계층(transport layer)에 부하를 주는 것에 그치지 않습니다. 기존의 멘탈 모델(mental model) 자체를 무효화합니다.

실제로 무엇이 깨지는지 설명하겠습니다.

1. 백프레셔 (Backpressure): 당신에게 존재하지 않는 병목 지점

중앙 OT 서버는 어떤 클라이언트든 아주 쉽게 조절(throttle)할 수 있습니다. 서버가 권한을 가진 주체이며 큐(queue)를 제어하기 때문입니다. 반면 CRDT 피어 모델에는 자연스러운 병목 지점이 없습니다. 이것이 당신이 Yjs를 선택했을 때 받아들인 트레이드오프(tradeoff)이며, 인간 피어들은 스스로 속도를 조절하기 때문에 보통은 문제가 되지 않습니다.

에이전트 피어는 스스로 속도를 조절하지 않습니다. 제한을 두지 않으면, 에이전트의 doc.transact() 호출이 동기화 사이클(sync cycle)을 범람시켜 인간의 속도로 발생하는 연산(operations)이 수렴 창(convergence window)을 확보하는 것을 방해합니다. 이것은 쓰기 기아(write starvation) 현상으로, 데이터베이스 동시성(database concurrency) 문제와 동일한 부류이며, 방에 있는 다른 모든 사람들에게 커서 지연(cursor lag)과 프레젠스(presence) 업데이트 누락으로 나타납니다.

해결책은 전송 계층(transport layer)에 있어서는 안 됩니다. LLM의 스트리밍 출력과 Yjs 문서 쓰기 사이에 위치해야 합니다:

// LLM 스트림과 Yjs 쓰기 사이의 토큰 버킷 (Token bucket)
const agentBucket = new TokenBucket({
  capacity: 50,     // 최대 큐에 쌓일 연산 수
...

위 수치는 예시일 뿐이며, 사용 중인 제공업체와 룸 크기에 맞춰 조정하십시오. 핵심은 속도 제한(rate limit)이 에이전트의 기원(origin)에 맞춰 애플리케이션 계층에 존재해야 한다는 것입니다. 그래야 모델이 얼마나 빨리 생성하든 상관없이 인간의 연산이 수렴 창의 할당량을 항상 보장받을 수 있습니다.

이 지점은 2026년에 CRDT 대 OT 논쟁이 다시 재점화되는 지점이기도 합니다. 피어 모델은 여전히 인간의 협업에는 적합합니다. 하지만 AI 에이전트를 위해, 당신은 가벼운 중앙 제약 조건을 다시 추가하고 있는 것입니다. 이는 정확성(correctness)을 위해서가 아니라 공정성(fairness)을 위해서입니다.

2. 실행 취소 기록 (Undo History): 당신이 이미 겪고 있을지도 모르는 기원 문제

y-undomanager는 origin(기원)별로 실행 취소 기록 (undo history)의 범위를 지정합니다. 이는 올바른 동작이며 문서화되어 있습니다. 하지만 "올바른" 것과 "의도적인" 것이 반드시 같은 것은 아닙니다.

만약 에이전트의 작업(operations)이 사용자의 origin과 공유된다면, Ctrl+Z는 동전 던지기처럼 예측 불가능해집니다. 만약 에이전트에게 별도의 origin을 부여한다면(마땅히 그래야 합니다), 이제 두 번째 질문이 생깁니다. 사용자에게 보여지는 실행 취소(undo)에 에이전트의 작업이 나타나야 하는가? 만약 그렇다면, 사용자의 자체 기록과 비교했을 때 어떤 순서로 나타나야 하는가?

보편적인 정답은 없지만, 명확한 원칙은 있습니다. 에이전트에게 자체적인 trackedOrigins를 가진 별도의 UndoManager를 부여하고, 에이전트의 실행 취소를 기본 Ctrl+Z 경로가 아닌 별도의 UI 기능(affordance)으로 노출하는 것입니다.

const userUndoManager = new Y.UndoManager(ytext, {
  trackedOrigins: new Set([userOrigin]),
});
...

이는 ProseMirror에 댓글 표시(comment marks)나 변경 사항 추적 표시(tracked-change marks)를 추가할 때 직면하는 것과 동일한 설계 결정입니다. 콘텐츠 그 자체라기보다 콘텐츠를 "설명"하는 표시(marks)는 사용자가 직접 제어하는 표시와 별도의 생명주기(lifecycle)를 가져야 합니다. 에이전트 피어(agent peer)는 이와 동일한 패턴의 문서 수준(document-level) 버전입니다.

만약 사용자가 다른 사람이 남긴 댓글 스레드를 실수로 실행 취소(undo)하는 상황을 겪어본 적이 있다면, 이미 이 문제를 체감한 것입니다. 해결책은 같습니다. 명령 핸들러(command handler) 내부에 if (origin === agentOrigin) return과 같이 숨겨진 암시적인 방식이 아니라, 매니저 수준에서 소유권 경계를 명시적으로 만드는 것입니다.

3. Presence 및 Awareness: 병합할 것인가, 침몰시킬 것인가

Awareness 프로토콜은 인간의 속도에 맞춘 커서 업데이트를 위해 설계되었습니다. 피어당 초당 몇 번의 브로드캐스트(broadcast)가 발생하는 것은 정상이며, 렌더링 레이어(rendering layer)가 이를 잘 처리합니다.

분당 3,000단어를 생성하는 에이전트는 인간이 시각적으로 처리할 수 없는 속도로 위치 변경을 생성합니다. 이 모든 것을 브로드캐스트하는 것은 네트워크 회선과 React 렌더링 사이클 모두에 노이즈를 발생시킵니다.

두 가지를 수행해야 합니다. 첫째, 에이전트 피어에 대해서는 작업(operation) 단위가 아닌 고정된 간격으로 awareness 업데이트를 병합(coalesce)하십시오.

let pendingAwarenessUpdate: ReturnType<typeof setTimeout> | null = null;

function updateAgentAwareness(pos: number) {
...

둘째, 렌더링 레이어(rendering layer)가 컴포넌트 곳곳에 조건부 로직을 흩뿌리지 않고도 에이전트를 인간 커서와 구분할 수 있도록, 에이전트의 인지 상태(awareness state)에 type 필드를 추가합니다:

provider.awareness.setLocalState({
  type: 'agent',
  streaming: true,
...

"AI가 작성 중"이라는 상태와 "다른 사람이 타이핑 중"이라는 상태는 서로 다른 어포던스(affordances)입니다. 이들은 서로 다른 시각적 처리와 서로 다른 업데이트 속도를 가져야 합니다. 이러한 차이점을 인지 상태(awareness state)에 인코딩하면, 렌더링 레이어가 한 곳에서 올바른 결정을 내릴 수 있습니다.

이것이 귀하의 RFC에 의미하는 바

에이전트를 피어(peer)로 취급하는 패턴이 올바른 아키텍처입니다. LLM을 Yjs에 연결하는 것은 어려운 부분이 아닙니다.

어려운 부분은 귀하의 협업 시스템이 피어 대칭성(peer symmetry)에 대해 가지는 모든 가정을 다시 검토하고, 그 가정들을 _명시적(explicit)_으로 만드는 것입니다. 그래야만 다른 모든 사용자에게는 영향을 주지 않으면서, 에이전트 피어에 대해서만 의도적으로 그 가정들을 깨뜨릴 수 있기 때문입니다.

구체적으로는 다음과 같습니다: 귀하의 백프레셔(backpressure) 전략은 단일 피어가 수렴 주기(convergence cycle)를 지배할 수 없다고 가정했습니다. 따라서 에이전트의 오리진(origin)에 범위가 지정된 애플리케이션 레이어의 토큰 버킷(token bucket)이 필요합니다. 귀하의 실행 취소 히스토리(undo history)는 추적되는 모든 오리진이 사용자에게 속한다고 가정했습니다. 따라서 에이전트는 별도의 UI 액션으로 노출되는 독립적인 UndoManager가 필요합니다. 귀하의 인지(awareness) 렌더링은 커서 업데이트가 인간의 속도로 도착한다고 가정했습니다. 따라서 에이전트의 존재(presence)는 병합(coalescing) 처리가 필요하며, 인지 상태(awareness state)에 타입 판별자(type discriminant)가 필요합니다.

이러한 문제들은 일단 이름을 붙이고 나면 구현하기 어렵지 않습니다. 진짜 위험은 이러한 문제들을 명명하지 않은 채 통합을 출시했다가, 실제 협업 부하가 발생하고 실행 취소 히스토리가 엉망이 된 6주 뒤에 사용자 보고를 통해 실패 모드(failure modes)를 발견하는 것입니다.

속도 제한(rate limiting), 실행 취소 격리(undo isolation), 그리고 존재 병합(presence coalescing)을 코드 리뷰에서 발견되는 예외 케이스가 아니라, RFC의 일급 항목(first-class line items)으로 취급하십시오.

2026년 4월 데모와 관련 저장소(repo)는 electric.ax/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs에서 확인할 수 있습니다. y-prosemirror + awareness 설정은 TipTap 스택과 직접적으로 매핑됩니다. 통합을 계획 중이라면 Yjs UndoManager 문서를 함께 읽어볼 가치가 있습니다.

왜 이것인가, 왜 지금인가: 2026년 4월 데모는 프로덕션급 스택(y-prosemirror, awareness 프로토콜, Durable Streams)에서 에이전트를 Yjs 피어(peer)로 사용하는 패턴을 구현한 최초의 작동하는 사례이며, 불과 몇 주 전에 공개되었습니다. 여기서 드러나는 "에이전트 속도 문제(agent velocity problem)"는 진정으로 새로운 문제입니다. CRDT 문헌에는 이 정도 규모의 비대칭적 피어 처리량(asymmetric peer throughput)에 대한 이전의 해답이 없으며, 현재 협업형 AI 편집 기능을 구축 중인 모든 팀은 동일한 세 가지 실패 모드(failure modes)에 직면하게 될 것입니다. 이 패턴이 잘못된 기본값(bad defaults)으로 굳어지기 전인 지금 이 글을 쓰는 것이 정확히 적절한 시기입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0