Skip to content
Published on

실시간 협업 & CRDT 2026 심층 가이드 — Liveblocks·PartyKit·Yjs·Automerge·Loro·ShareDB·Replicache·Fluid·Tldraw sync

Authors

프롤로그 — 2026년, "동시에 편집한다"는 것의 의미

2010년 Google Docs가 "동시에 같은 문서를 편집한다"를 일반 사용자에게 선물했을 때, 그 기술은 OT(Operational Transform)였다. 한 사람의 편집 연산을 다른 사람의 편집 연산에 맞춰 "변환"하는 방식. 동작은 했지만, 서버 중심이었고, 오프라인은 어려웠고, 잘못된 변환 함수는 영원히 디버깅해야 했다.

2026년 5월, 풍경이 완전히 바뀌었다. CRDT(Conflict-free Replicated Data Type) 가 웹 협업의 기본 단위가 되었다. Figma의 멀티플레이어 인프라, Linear의 로컬-퍼스트 동기화, Notion의 라이브 커서, tldraw의 실시간 캔버스, Excalidraw의 룸 동기화 — 이 모든 것이 CRDT 또는 그 변형 위에 서 있다.

그리고 더 큰 변화 두 가지가 더 있다.

  • 2025년 11월, Vercel이 Liveblocks와 PartyKit을 잇따라 협력 파트너로 끌어들이며 "Vercel for Realtime"이라는 포지셔닝을 굳혔다.
  • Ink & Switch 연구실이 발표한 "Local-first software" 매니페스토는 더 이상 학술 호기심이 아니다. Automerge 2.x·Loro·Yrs(Yjs의 Rust 포트)가 실서비스에 들어갔다.

이 글은 2026년의 실시간 협업 스택을 한 호흡으로 정리한다. 이론(CRDT vs OT, 벡터 클럭, Lamport)부터 라이브러리 비교(Yjs vs Automerge vs Loro), SaaS 비교(Liveblocks vs PartyKit vs Hocuspocus), 운영 패턴(WebSocket vs WebRTC, awareness, 영속화)까지.


1장 · 동시 편집의 본질 — 같은 문서, 다른 시간선

먼저 문제를 정의하자. 두 사람이 같은 문서를 동시에 편집한다는 건, 컴퓨터과학적으로는 "같은 상태에 두 개의 독립적인 연산이 가해진다"는 뜻이다.

시각 t0:    "Hello"           (서버 = 클라A = 클라B)

시각 t1:    클라A가 5번 위치에 ", world" 삽입
시각 t1:    클라B가 0번 위치에 "Say: " 삽입
            (서로 모름)

시각 t2:    두 연산이 서버에서 만남
            -> 결과가 무엇이어야 하나?

답은 두 가지다. OT는 "두 연산을 서로의 좌표계에 맞게 변환"한다. CRDT는 "좌표를 처음부터 절대적·고유한 ID로 매기고 연산을 그대로 적용"한다.

OT는 서버라는 단일 진실의 원천을 가정하지만, CRDT는 서버 없이도 모든 클라이언트가 결국 같은 상태에 수렴한다(이를 eventual consistency라 한다). 로컬-퍼스트 소프트웨어의 전제 조건이 바로 이것이다.


2장 · CmRDT vs CvRDT — 연산이냐 상태냐

CRDT는 크게 두 종류로 나뉜다.

  • CmRDT(Operation-based): 연산(operation)을 전파한다. "5번 위치에 'x' 삽입"이라는 메시지를 보낸다. 메시지 크기는 작지만, 정확히 한 번 전달(exactly-once) 또는 인과 순서(causal order) 보장이 필요하다.
  • CvRDT(State-based): 상태(state) 전체나 일부를 전파한다. 동기화 시점에 머지(merge) 함수가 두 상태를 결합한다. 머지 함수는 교환·결합·멱등(commutative, associative, idempotent) 이어야 한다.

Yjs와 Automerge는 둘 다 op-based로 시작했지만, 2026년의 Loro·Y-CRDT·Automerge 2.x는 delta-state 또는 하이브리드 모델로 진화했다. 변경분만 보내되, 멱등하게 처리할 수 있는 형태로.

종류메시지 크기네트워크 보장대표 라이브러리
CmRDT작음인과 순서 + at-least-once초기 Treedoc, RGA
CvRDTbest-effort, 멱등초기 Riak DT
Delta-state중간멱등 + 인과 메타Yjs, Automerge 2.x, Loro

3장 · OT vs CRDT — 왜 CRDT가 이겼나

기준OTCRDT
중앙 서버 필요사실상 필요불필요 (P2P 가능)
오프라인 편집어려움자연스러움
변환 함수 검증매우 어려움(TP2 문제)불필요
메모리 사용작음큼 (메타데이터 누적)
텍스트 CRDT 성숙도2000년대 검증됨2020년대 검증됨
대표 사용처Google Docs (역사적)Figma, Linear, Notion 라이브 커서

Google Docs는 여전히 OT를 쓴다(레거시 + 검증된 인프라). 하지만 2020년대 이후 신규 협업 앱은 거의 모두 CRDT를 택했다. 가장 큰 이유는 오프라인 지원P2P 가능성이다.


4장 · 벡터 클럭과 Lamport timestamp — 분산 시간의 두 얼굴

분산 시스템에서는 절대 시간이 없다. 그래서 두 가지 도구가 쓰인다.

  • Lamport timestamp: 모든 이벤트에 단조 증가하는 정수를 부여한다. 두 노드의 시간을 비교하려면 max + 1. 전순서(total order)는 만들 수 있지만, 두 이벤트가 동시인지(concurrent) 알 수 없다.
  • Vector clock: 노드별 카운터를 모두 들고 다닌다. [A:3, B:2, C:1]. 두 벡터를 비교하면 "선행/후행/동시" 셋 중 하나를 확정할 수 있다.

Yjs는 각 클라이언트가 고유 ID를 받고, ID + 단조 시퀀스의 조합으로 연산을 식별한다 (사실상 Lamport + client-id). Automerge는 액터(actor) ID와 시퀀스 번호의 조합 (벡터 클럭의 변형)을 쓴다. Loro도 actor + counter 모델.


5장 · 텍스트 CRDT 이론 — Yjs Y.Text의 내부

텍스트는 CRDT 중에서 가장 어렵다. "5번 위치에 'x' 삽입"이라는 인덱스 기반 연산은 동시 편집에서 무너진다(다른 사람이 4번에 뭔가 끼우면 5번이 6번이 된다). 그래서 CRDT는 고유 ID 기반 위치 지정을 쓴다.

Yjs의 Y.Text는 모든 문자에 (clientID, clock) 쌍을 부여하고, 이를 이중 연결 리스트(double-linked list of Items) 로 잇는다. 삽입은 "이 ID 다음에 끼워라"가 되고, 삭제는 tombstone(삭제 표시)으로 남는다. 동일 위치 동시 삽입은 clientID로 결정적 순서를 만든다.

// Yjs: 같은 문서를 두 사람이 편집한다
import * as Y from 'yjs'

// 클라이언트 A
const docA = new Y.Doc()
const textA = docA.getText('content')
textA.insert(0, 'Hello')

// 클라이언트 B (오프라인에서)
const docB = new Y.Doc()
const textB = docB.getText('content')
Y.applyUpdate(docB, Y.encodeStateAsUpdate(docA))
textB.insert(5, ', world')

// 동시에, 클라이언트 A에서
textA.insert(0, 'Say: ')

// 양쪽 변경분을 서로에게 전달
Y.applyUpdate(docA, Y.encodeStateAsUpdate(docB))
Y.applyUpdate(docB, Y.encodeStateAsUpdate(docA))

// 두 문서가 같은 상태로 수렴
console.log(textA.toString()) // "Say: Hello, world"
console.log(textB.toString()) // "Say: Hello, world"

이 코드의 핵심은 encodeStateAsUpdateapplyUpdate다. 어떤 순서로, 몇 번을, 어떤 경로로 적용하든 결과는 같다. 멱등하고, 교환 가능하고, 결합 가능하다.


6장 · Yjs의 내부 구조 — Item 링크드 리스트와 GC

Yjs의 핵심 자료구조는 Item이다. 각 Item은:

  • id: { client: number, clock: number } — 고유 식별자
  • origin: ID | null — "이 Item이 어느 Item 다음에 삽입되었는가"
  • rightOrigin: ID | null — 동시 삽입 충돌 해결용
  • content: ContentString | ContentEmbed | ... — 실제 값
  • deleted: boolean — tombstone 플래그

Yjs는 메모리 폭증을 막기 위해 GC를 한다. 모든 클라이언트가 본 삭제는 tombstone에서 실제 제거된다. 또한 인접한 동일-클라이언트 Item을 블록 단위로 압축한다(예: 한 번에 'Hello'를 친 5개 Item을 하나로).

성능 벤치마크에서 Yjs는 일관되게 가장 빠른 텍스트 CRDT 중 하나다. 100만 글자 문서에서도 메모리·CPU가 실용 수준에 머문다.


7장 · Automerge — JSON CRDT의 표준

Automerge는 Yjs와 같은 시기에 시작했지만 JSON 문서 전체를 CRDT로 다루는 데 초점을 둔다. 텍스트도 지원하지만(Automerge.Text → 최근에는 splice 기반 API), 강점은 깊게 중첩된 객체 트리의 머지다.

// Automerge: 같은 JSON 문서를 두 사람이 편집한다
import { from, change, save, load, merge } from '@automerge/automerge'

// 초기 문서
let doc1 = from({
  title: 'Realtime collab notes',
  todos: [{ done: false, text: 'Write CRDT post' }],
})

// 직렬화 후 다른 클라이언트로 전달
const binary = save(doc1)
let doc2 = load(binary)

// 양쪽 동시 편집
doc1 = change(doc1, (d) => {
  d.todos.push({ done: false, text: 'Add references' })
  d.title = 'Realtime collab notes (draft)'
})

doc2 = change(doc2, (d) => {
  d.todos[0].done = true
})

// 양쪽 머지
const merged1 = merge(doc1, doc2)
const merged2 = merge(doc2, doc1)

// 두 결과는 동일
console.log(JSON.stringify(merged1) === JSON.stringify(merged2)) // true

Automerge 2.x는 Rust로 다시 쓰였고(automerge-rs), WASM으로 컴파일되어 브라우저·Node·Electron에서 모두 돈다. 메모리·속도가 1.x 대비 한 자릿수 좋아졌다.


8장 · Loro — 2024-2026의 다크호스

Loro는 중국 발 신생 CRDT 라이브러리로, 2024년에 1.0을 찍고 2026년에는 Yjs·Automerge와 함께 "큰 세 개"로 묶인다. 특징은:

  • 처음부터 Rust + WASM
  • 시간 여행(time travel) 을 1급 기능으로 — 임의 시점의 문서 상태로 되돌아가기
  • 리치 텍스트 마크 CRDT(스타일 범위)를 별도 자료구조로 정밀하게 처리
  • 트리 CRDT(부모-자식 관계의 동시 이동)를 진지하게 다룸

벤치마크에 따르면 Loro는 특정 워크로드(특히 트리·리치 텍스트)에서 Yjs보다 빠르다. 한편 Yjs는 텍스트 단순 편집·생태계·교재 자료에서 여전히 우위다.


9장 · Liveblocks — CRDT-as-a-Service의 첫 번째 챔피언

Liveblocks(파리 기반)는 "멀티플레이어 인프라를 SaaS로" 라는 명제를 가장 먼저 잡았다. 2026년 기준 주요 기능:

  • LiveObject·LiveList·LiveMap — 자체 CRDT (Yjs와 별도)
  • useStorage / useMutation — React 훅으로 상태 동기화
  • Awareness/Presence — 커서·이름·선택 영역 실시간 공유
  • Comments·Threads — 문서 위에 코멘트 다는 별도 모듈
  • Yjs 어댑터 — 기존 Yjs 앱(예: Tiptap·BlockNote·Lexical Yjs 어댑터)도 Liveblocks 트랜스포트로 동기화 가능
// Liveblocks: useStorage 훅으로 공유 상태 읽고 쓰기
import { useMutation, useStorage } from '@liveblocks/react/suspense'

function Todos() {
  const todos = useStorage((root) => root.todos)

  const addTodo = useMutation(({ storage }, text: string) => {
    const list = storage.get('todos')
    list.push({ id: crypto.randomUUID(), text, done: false })
  }, [])

  const toggle = useMutation(({ storage }, id: string) => {
    const list = storage.get('todos')
    const idx = list.findIndex((t) => t.id === id)
    if (idx >= 0) {
      const item = list.get(idx)
      list.set(idx, { ...item, done: !item.done })
    }
  }, [])

  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id} onClick={() => toggle(t.id)}>
          {t.done ? 'X' : 'O'} {t.text}
        </li>
      ))}
    </ul>
  )
}

Liveblocks의 강점은 운영의 단순함React에 최적화된 DX다. 약점은 가격(시트당·MAU당 과금)과 자체 CRDT에 잠금(완전 자가 호스팅은 어려움).


10장 · PartyKit — Cloudflare Durable Objects 위의 협업

PartyKit(2023년 Sunil Pai 창립, 2024년 Cloudflare 인수)은 다른 길을 간다. 각 협업 룸이 하나의 Durable Object이고, 그 안에 임의 코드(Yjs·Automerge·자체 로직)를 실행한다.

// PartyKit: 서버 코드 (server.ts)
import type * as Party from 'partykit/server'
import { onConnect } from 'y-partykit'

export default class YjsServer implements Party.Server {
  constructor(readonly room: Party.Room) {}

  async onConnect(conn: Party.Connection) {
    // y-partykit이 Yjs 동기화를 자동 처리
    return onConnect(conn, this.room, {
      persist: { mode: 'snapshot' },
    })
  }
}

PartyKit의 강점은 사용자 코드를 엣지에서 실행한다는 점이다. 룸 단위로 인증·권한·영속화·게임 로직을 자유롭게 짤 수 있다. 약점은 Cloudflare 잠금(이 부분은 2026년 와서는 사실상 장점이기도 함 — 글로벌 엣지 분산이 공짜).


11장 · 자가 호스팅 — Hocuspocus + Yjs

비용·잠금이 부담이면 Hocuspocus가 답이다. Tiptap 팀이 만든 Yjs 백엔드로, Node.js에서 돌고, Postgres·SQLite·Redis로 영속화한다.

// Hocuspocus 서버
import { Server } from '@hocuspocus/server'
import { Database } from '@hocuspocus/extension-database'

const server = new Server({
  port: 1234,
  extensions: [
    new Database({
      fetch: async ({ documentName }) => {
        // DB에서 Yjs 바이너리 로드
        const row = await db.documents.findUnique({ where: { name: documentName } })
        return row?.state ?? null
      },
      store: async ({ documentName, state }) => {
        // DB에 저장
        await db.documents.upsert({
          where: { name: documentName },
          update: { state, updatedAt: new Date() },
          create: { name: documentName, state },
        })
      },
    }),
  ],
  async onAuthenticate({ token, documentName }) {
    // JWT 검증 등
    if (!verifyToken(token)) throw new Error('Unauthorized')
    return { userId: extractUserId(token) }
  },
})

server.listen()

운영을 직접 잡을 수 있고 데이터 주권이 보장된다. 대신 스케일링·HA·모니터링은 직접 한다. Notion·Linear급으로 가면 결국 자체 인프라를 짜게 된다.


12장 · ShareDB — OT 진영의 생존자

ShareDB(전 derby-share)는 OT 기반의 자가 호스팅 라이브러리다. Yjs 이전 시대(2015 전후) 협업 앱에서 폭넓게 쓰였고, 지금도 OT가 익숙한 팀에서는 현역이다.

  • 문서를 JSON으로 표현하고, JSON OT 연산을 전파
  • MongoDB·Postgres에 영속화
  • 텍스트 OT(json0·json1 타입)는 검증됨
  • 약점: P2P·오프라인이 CRDT만큼 자연스럽지 않음

2026년 신규 프로젝트라면 CRDT(Yjs/Automerge/Loro)를 택하는 게 일반적 권장이지만, 이미 ShareDB로 잘 굴러가는 시스템을 굳이 갈아엎을 이유는 없다.


13장 · Replicache·Zero — "로컬-퍼스트 동기화 엔진"의 다른 답

Rocicorp의 Replicache(2020-)와 그 후속 Zero(2025-)는 CRDT가 아니다. mutator 기반 동기화 + 서버 권위 + 낙관적 UI라는 모델이다.

  • 클라이언트가 "mutator" 함수를 정의한다(예: addTodo(text)).
  • 로컬에서 mutator를 즉시 실행하고 UI를 업데이트.
  • 같은 mutator 요청을 서버에 보내고, 서버에서 권위적으로 다시 실행.
  • 서버 결과를 받으면 로컬 결과가 덮어쓰기됨(롤백 가능).

CRDT가 "여러 진실이 결국 수렴"이라면, Replicache는 "서버가 최종 진실, 클라이언트는 낙관적 추측"이다. Linear가 이 모델로 유명해졌다.


14장 · Fluid Framework — Microsoft의 엔터프라이즈 답

Fluid Framework는 Microsoft 365(Office Online·Loop)의 실시간 협업 엔진이다. CRDT는 아니지만 시퀀스 기반의 자체 동기화 모델이다.

  • SharedString·SharedMap·SharedTree 등의 분산 자료구조(DDS)
  • Azure Fluid Relay 위에서 도는 서비스
  • 외부 개발자가 쓰기는 어렵고(공식 SDK는 있음), 사실상 Microsoft 내부 엔진

2026년 외부 신규 앱이 Fluid를 채택할 가능성은 낮지만, Office 통합·Microsoft Graph 위에서 협업을 짠다면 알아둘 가치가 있다.


15장 · 화이트보드 협업 — Tldraw와 Excalidraw

tldraw(@steveruizok·@orta)는 화이트보드·다이어그램 라이브러리이고, 멀티플레이어 sync 엔진을 자체 개발했다. 내부적으로는 Yjs와 비슷한 op-based 모델이지만 캔버스 자료구조(셰이프 트리)에 최적화되어 있다. @tldraw/sync 패키지로 React + WebSocket 서버로 룸 동기화하고, Cloudflare Durable Objects 위에서 도는 레퍼런스 구현이 있으며, 셰이프 단위의 마지막 작성자 우선(LWW)으로 그림 도구의 의미론에 맞춰져 있다.

반면 Excalidraw는 손그림 화이트보드의 대명사이고, 협업 룸 기능은 암호화된 Firebase Realtime Database 위에서 돈다. CRDT를 정식으로 안 쓰고, 단순한 LWW와 클라이언트 측 변경 머지로 처리한다. 의외로 합리적이다. 화이트보드는 짧은 세션 동안 적은 수의 셰이프를 동시 편집한다. CRDT의 메타데이터 오버헤드가 필요 없는 도메인이다.

tldraw의 교훈: CRDT는 자료구조에 따라 다르게 디자인된다. 텍스트 CRDT(Yjs Y.Text)와 캔버스 CRDT(tldraw)는 같은 이론 위에 있어도 구현 디테일이 전혀 다르다. 모든 협업 앱이 풀 CRDT를 깔아야 하는 건 아니라는 점.


16장 · Figma의 멀티플레이어 아키텍처

Figma는 2016-2017년에 자체 멀티플레이어 엔진을 만들었다. CRDT의 한 변형으로, 노드·속성 단위로 LWW를 적용한다. 핵심은:

  • 도큐먼트 = 노드 트리, 각 노드는 속성 사전(property bag)
  • 각 속성에 최신 변경의 (timestamp, clientID) 부여
  • 충돌 = 더 큰 timestamp 승리(LWW), 동률은 clientID 큰 쪽
  • 트리 구조 변경(부모 교체, 순서 변경)은 별도 트리 CRDT 로직

Figma의 엔지니어 Evan Wallace의 2019년 글이 이 아키텍처를 공개했고, 이후 많은 협업 앱이 비슷한 패턴을 따랐다. tldraw도 일부 영향을 받았다.


17장 · Google Docs와 OT의 역사

Google Docs는 2010년 Writely 인수로 시작해 2010년에 동시 편집을 정식 출시했다. 엔진은 Jupiter(Google 내부 이름) OT다. 핵심 아이디어:

  • 모든 편집은 (revision, operation) 쌍
  • 클라이언트는 서버에 op을 보내고, 서버는 다른 op과 변환해서 받음
  • TP1(Transformation Property 1): op1 \* (op2 transformed against op1) == op2 \* (op1 transformed against op2) 가 성립해야 함

OT의 변환 함수를 정확히 짜는 건 악명 높게 어렵다. Google이 십수 년에 걸쳐 다듬은 이 코드는 2020년대 신규 앱이 따라가기에는 비용이 너무 컸고, 그래서 업계는 CRDT로 이동했다.


18장 · Presence / Awareness — "누가 어디서 무엇을 보고 있는가"

협업의 또 다른 절반은 presence다. 누가 온라인이고, 어디 커서가 있고, 무엇을 선택했고, 어떤 색깔로 표시되는가.

Yjs는 y-protocols/awareness로 표준화했다. CRDT 본체와 별개의 짧은 수명 상태(ephemeral state)다. 메타데이터:

  • clientID (Y.Doc과 공유)
  • name, color, cursor, selection 등 자유 JSON
  • 하트비트로 살아있음 표시, 끊기면 자동 정리

Liveblocks는 awareness를 1급 시민으로 들고 와 useOthers()·useUpdateMyPresence()로 노출한다. PartyKit도 자체 broadcast로 구현 가능. 어느 쪽이든 presence는 영속화하지 않는 것이 원칙 — 룸이 닫히면 사라진다.


19장 · WebSocket vs WebRTC — 트랜스포트의 선택

CRDT 동기화의 트랜스포트는 두 갈래다.

기준WebSocketWebRTC (DataChannel)
서버 필요필요NAT traversal용 STUN/TURN만
지연서버 라운드트립직통 P2P (빠를 수 있음)
확장성서버가 팬아웃메시 토폴로지면 N^2
영속화자연스러움서버 미러 필요
방화벽보통 잘 통과TURN 없으면 막힐 수 있음
표준 라이브러리y-websocket, y-partykity-webrtc

2026년의 일반적 권장: WebSocket을 기본으로 하되, 진짜 P2P가 필요한 도메인(보안, 오프라인 메시) 또는 서버 부하를 분산하고 싶을 때 WebRTC를 보조로 쓴다.


20장 · CRDT 라이브러리 비교

라이브러리언어강점약점대표 사용처
YjsJS/TS (+Yrs Rust 포트)성숙·생태계·텍스트 성능본체 메모리 다소 큼Tiptap, BlockNote, Lexical Yjs
AutomergeRust/WASMJSON 트리 머지 강함텍스트 성능은 Yjs보다 약함PushPin, Trail Runner
LoroRust/WASM리치 텍스트·트리·시간여행생태계 신생신규 앱들
Y.js + YrsRust 포트Rust 네이티브 백엔드Yjs와 와이어 호환 보장은 부분적서버 사이드 Yjs
Diamond TypesRust단일 텍스트 CRDT 성능 챔피언JSON 미지원벤치마크용
sjs (Synchronized JS)실험TypeScript 친화신생연구용

21장 · SaaS vs 자가 호스팅 비교

기준LiveblocksPartyKitHocuspocus (자가)ReplicacheFluid (MS)
CRDT 종류자체 + Yjs 어댑터Yjs/Automerge 자유Yjs비-CRDT(낙관적)DDS (비-CRDT)
호스팅SaaSCloudflare 엣지셀프셀프 또는 RocicorpAzure
가격MAU 기반사용량 기반인프라 비용만사용량 + SaaSAzure 단가
자가 호스팅 가능부분(Edge Storage 옵션)어려움 (CF 종속)완전Replicache는 가능어려움
데이터 주권미국·EU 옵션글로벌 엣지자유자유Azure 리전
Awareness 1급직접 구현미해당부분
코멘트/스레드 모듈없음없음없음Loop이 별도

22장 · 한국·일본 사례 — 노션, 카카오, 사이보즈, Sansan

한국의 협업 시장은 흥미롭다. 노션 한국 진출(2020) 이후 Notion API와 자체 빌드 협업의 비율이 빠르게 늘었다(노션은 자체 CRDT-like 모델을 쓰지만 공개 문서는 적다). 카카오워크 / 카카오톡의 메시지는 CRDT가 아니지만, 카카오워크의 게시판·메모 협업은 OT 기반(공개 발표)이며, 카카오엔터프라이즈가 "원격 협업"을 키운 시기에 자체 엔진을 만들었다. 네이버 라인웍스(Line Works) 는 문서·표·캘린더 협업을 제공하며, 라인의 일본 시장과 통합되어 일본 엔터프라이즈에서 점유율이 크다. 토스 / 토스페이먼츠는 내부 협업 툴(아카이브·문서)에서 Yjs + Tiptap 조합을 채택한 사례를 콘퍼런스에서 공유했다.

일본도 흥미롭다. Cybozu kintone은 비즈니스 앱 빌더의 폼·레코드 협업으로, 같은 레코드를 여러 사용자가 동시에 편집할 때 OT 기반 변환을 적용한다. 사이보즈는 오랜 OT 노하우를 가진 회사로 유명하다. Sansan은 명함 클라우드로, 명함 데이터 OCR 결과를 여러 사용자가 동시에 보정하는 워크플로에서 CRDT 적용을 발표한 바 있다. 라쿠텐 RMS는 라쿠텐 쇼핑몰의 점주용 협업 툴로 내부적으로 자체 동기화 엔진을 쓴다. Notion 일본팀은 영어권과 동일한 엔진이지만, 일본어 IME와의 협업 충돌(조합 중인 글자)을 다루는 패치가 일본팀 PM 발표로 공개되었다.


23장 · CRDT 도입 의사결정 매트릭스

언제 CRDT가 정답이고 언제 아닌가.

상황권장
텍스트 동시 편집(에디터)Yjs + Tiptap/BlockNote/Lexical-Yjs
화이트보드/캔버스tldraw sync 또는 Yjs Y.Map
폼·테이블(셀 단위)Yjs Y.Map + 셀별 키 또는 ShareDB
게임 상태(시간 결정적)CRDT보다 lock-step 또는 권위 서버
채팅 메시지CRDT 불필요, append-only 로그
알림·실시간 카운터CRDT G-Counter (학술적이지만) 또는 Redis
오프라인 1순위 (로컬-퍼스트)Automerge, Loro, Yjs IndexedDB persistence
서버 권위가 중요 (결제·재고)Replicache 또는 전통 트랜잭션

24장 · 영속화 전략 — IndexedDB·Postgres·바이너리 스냅샷

CRDT는 어딘가에 저장되어야 한다.

  • 클라이언트 측: Yjs의 y-indexeddb, Automerge의 IndexedDB 어댑터. 오프라인 편집 후 재접속 시 자동 동기화.
  • 서버 측 (Yjs 바이너리): Yjs는 encodeStateAsUpdate로 전체 상태를 바이너리로 저장한다. Postgres bytea 컬럼, S3 객체, Redis 등 어디든 가능.
  • 서버 측 (델타 로그): 매 업데이트를 로그로 누적하고 주기적으로 스냅샷 압축. 대용량 문서에 유리.
  • 하이브리드: 스냅샷 + 그 이후 델타. 로딩 시 스냅샷 + 델타 누적.

영속화의 함정은 GC다. 모든 클라이언트가 본 tombstone은 지울 수 있지만, "모든 클라이언트"를 어떻게 판단하느냐가 어렵다. Yjs는 보수적으로 GC하고, 운영자가 주기적으로 명시적 압축을 호출할 수도 있다.


25장 · 보안·권한·종단간 암호화

협업 SaaS의 보안 모델은 세 갈래다.

  • 서버가 평문을 본다 (가장 흔함): Liveblocks·Hocuspocus 표준 모델. 권한 검사·검색·코멘트 모더레이션이 쉽다.
  • 종단간 암호화 (E2EE): 클라이언트가 Yjs 업데이트를 암호화한 뒤 서버에 보냄. 서버는 바이너리 블롭만 알고 의미를 모름. Excalidraw 룸이 비슷한 모델. 약점: 검색·코멘트 분석·서버 측 머지가 불가능.
  • 부분 암호화: 콘텐츠는 E2EE, 메타데이터(룸 멤버, presence)는 평문. 절충안.

권한은 보통 룸 단위 토큰 + 연산 단위 검증의 조합이다. 누가 룸에 들어올 수 있나(JWT), 들어와서 무엇을 할 수 있나(Hocuspocus의 onAuthenticate + beforeHandleMessage).


26장 · 에필로그 — 2027년을 본다

CRDT는 이제 학술의 영역을 떠나 운영의 영역에 있다. 2027년에 일어날 변화 셋:

  1. 모바일·네이티브 1급 시민화 — Yjs Swift 포트, Automerge Kotlin, Loro Flutter 바인딩이 모두 안정화되어 모바일 협업 앱이 폭발한다. 로컬-퍼스트가 진짜로 가능해진다.
  2. AI 에이전트가 협업 참여자가 된다 — Cursor·Claude Code가 같은 문서에 동시 편집하는 또 하나의 "유저"가 되고, awareness는 사람과 AI를 구분한다.
  3. 표준화의 시작 — IETF·W3C 수준에서 CRDT 와이어 포맷이 표준화 논의에 들어간다. Yjs·Automerge·Loro의 와이어 호환성은 아직 없지만, "표준 텍스트 CRDT"의 윤곽이 보이기 시작한다.

가장 큰 교훈은 단순하다. 저장 버튼이 사라진 세계는 이미 와 있다. 이제 남은 일은, 그 위에 무엇을 짓느냐다.


References