Add a collaborative document editor to your app
Quick Summary
단순한 <textarea 로는 동시 편집이 충돌하므로, Convex와 ProseMirror 기반 동기화 컴포넌트, Tiptap 또는 BlockNote를 조합해 기존 앱에 협업 문서 편집기를 붙이는 절차를 설명한다.
🖼️ 인포그래픽
🖼️ 4컷 인포그래픽
💡 한 줄 요약
단순한 <textarea>로는 동시 편집이 충돌하므로, Convex와 ProseMirror 기반 동기화 컴포넌트, Tiptap 또는 BlockNote를 조합해 기존 앱에 협업 문서 편집기를 붙이는 절차를 설명한다.
📌 핵심 요약
- 글은 금요일 오후에 제품 요구사항으로 간단한 공유 텍스트 영역을 추가했다가, 여러 사용자의 변경이 서로 덮어써지는 문제를 마주하는 상황에서 출발한다.
- 해결책으로 Convex를 백엔드 플랫폼으로 사용하고, @convex-dev/prosemirror-sync 컴포넌트를 통해 ProseMirror 문서의 증분 변경을 동기화하는 구성을 제안한다.
- 프런트엔드 편집기는 Tiptap 또는 BlockNote 중 선택할 수 있으며, 둘 다 ProseMirror 계열이지만 데이터 모델 차이 때문에 공개 출시 전 선택을 신중히 실험해야 한다고 설명한다.
- 문서 생성은 클라이언트 측 또는 서버 측에서 할 수 있고, 기존 테이블의 ID나 UUID 같은 문자열을 문서 ID로 삼아 제품 데이터와 협업 문서를 연결할 수 있다.
- 서버에서는 읽기와 쓰기 권한을 checkRead, checkWrite 콜백으로 검증하고, 편집 중 생성되는 스냅샷을 활용해 전체 텍스트 검색이나 벡터 검색용 파생 데이터를 효율적으로 만들 수 있다.
🧩 주요 포인트
- 글은 금요일 오후에 제품 요구사항으로 간단한 공유 텍스트 영역을 추가했다가, 여러 사용자의 변경이 서로 덮어써지는 문제를 마주하는 상황에서 출발한다.
- 해결책으로 Convex를 백엔드 플랫폼으로 사용하고, @convex-dev/prosemirror-sync 컴포넌트를 통해 ProseMirror 문서의 증분 변경을 동기화하는 구성을 제안한다.
- 프런트엔드 편집기는 Tiptap 또는 BlockNote 중 선택할 수 있으며, 둘 다 ProseMirror 계열이지만 데이터 모델 차이 때문에 공개 출시 전 선택을 신중히 실험해야 한다고 설명한다.
- 문서 생성은 클라이언트 측 또는 서버 측에서 할 수 있고, 기존 테이블의 ID나 UUID 같은 문자열을 문서 ID로 삼아 제품 데이터와 협업 문서를 연결할 수 있다.
- 서버에서는 읽기와 쓰기 권한을 checkRead, checkWrite 콜백으로 검증하고, 편집 중 생성되는 스냅샷을 활용해 전체 텍스트 검색이나 벡터 검색용 파생 데이터를 효율적으로 만들 수 있다.
🧠 상세 정리
1. 단순 텍스트 영역이 협업 편집에 실패하는 이유
글은 제품 매니저가 기존 기능에 편집 가능한 텍스트 영역을 추가해 달라고 요청하는 장면으로 시작한다. 개발자는 사용자가 서로 맥락을 공유하고 정보를 적어둘 수 있도록 간단한 <textarea>와 기존 데이터베이스 테이블의 문자열 필드를 붙이는 방식으로 빠르게 구현한다. 하지만 두 사람이 동시에 사용해 보자 한 사용자가 변경할 때 다른 사용자의 내용이 덮어써지는 문제가 드러난다. 매 키 입력마다 새 텍스트를 서버에 제출하더라도 전체 문자열을 계속 덮어쓰는 방식으로는 실제 협업 편집을 만들기 어렵다는 점이 핵심 문제로 제시된다.
2. 글이 다루는 협업 편집기 통합 범위
본문은 기존 제품 안에 협업 텍스트 편집기를 추가하려는 개발자를 대상으로 단계별 구현 흐름을 안내한다. 목표는 단순히 문서 내용을 저장하는 것이 아니라, 사용자의 증분 변경을 동기화하고 기존 제품 데이터와 문서를 함께 저장하며 서버에서 접근 권한을 검증하는 것이다. 또한 Tiptap 또는 BlockNote를 활용해 Notion이나 Dropbox Paper와 비슷한 마크다운 스타일의 풍부한 텍스트 편집 경험을 제공하는 방향을 제시한다. 마지막으로 주기적 스냅샷을 사용해 전체 텍스트 검색 같은 기능을 효율적으로 지원하는 방법까지 포함한다.
3. Convex를 백엔드로 추가하는 첫 단계
작성자는 협업 편집 데이터의 저장과 동기화를 위해 Convex를 사용하는 것으로 시작하라고 설명한다. 기존 프로젝트에 Convex를 추가하거나 npm create convex@latest로 새 프로젝트를 시작할 수 있으며, convex dev를 실행하면 프로젝트 안에 convex 폴더가 만들어져 백엔드 코드와 스키마를 정의하게 된다. 예시에서는 --tail-logs 옵션을 붙여 서버 로그가 터미널에 표시되도록 하는 방식을 선호한다고 말한다. 프런트엔드에서는 사용하는 프레임워크와 인증 방식에 맞춰 Convex React provider를 추가해야 하며, Clerk를 사용하는 경우에는 별도 안내가 있다고 언급한다.
4. ProseMirror 동기화 컴포넌트 설정
문서 동기화에는 Convex Component인 @convex-dev/prosemirror-sync를 사용한다. 이 컴포넌트는 ProseMirror 문서의 내부 데이터 모델을 동기화하는 역할을 하며, Tiptap과 BlockNote 모두 ProseMirror를 기반으로 한다는 점이 설명된다. 설정 과정에서는 convex.config.ts에서 prosemirrorSync 컴포넌트를 앱에 등록하고, convex/prosemirror.ts에서 ProsemirrorSync 인스턴스를 만든 뒤 syncApi가 제공하는 서버 함수를 내보낸다. 이 함수들에는 getSnapshot, submitSnapshot, latestVersion, getSteps, submitSteps가 포함되며, 이후 클라이언트 편집기에서 이 API를 사용해 문서 상태와 변경분을 주고받는다.
5. BlockNote와 Tiptap 중 편집기 선택
프런트엔드 편집기 선택지로는 Tiptap과 BlockNote가 제시된다. Tiptap은 ProseMirror 기반 편집기를 직접 쓰는 것보다 설정 부담을 줄여 주며, 확장 생태계가 풍부하고 사용자 정의가 쉽다는 장점이 있다. BlockNote는 Tiptap 위에 만들어져 있으며 기본 UI와 사용 경험이 더 잘 갖춰져 있어 복잡한 서식 기능을 빠르게 제공하기 좋지만, 고급 사용 사례에서는 확장과 커스터마이징이 더 어렵다고 설명된다. 둘 다 ProseMirror 계열이지만 데이터 모델이 달라 나중에 쉽게 갈아타기 어렵기 때문에, 공개 출시 전 두 방식을 실험해 보는 것이 권장된다.
6. UI에 편집기를 붙이고 문서 생성을 처리하는 방식
본문은 BlockNote와 Tiptap 각각에 대해 필요한 패키지를 설치하고 간단한 편집기 컴포넌트를 만드는 예시를 제공한다. BlockNote 예시에서는 useBlockNoteSync를 사용해 특정 ID의 문서를 동기화하고, 문서가 없으면 빈 doc 구조를 create로 생성한 뒤 BlockNoteView를 렌더링한다. Tiptap 예시에서는 useTiptapSync, EditorProvider, StarterKit, sync extension을 조합해 초기 콘텐츠가 있을 때 편집기를 렌더링하고, 없을 때는 문서를 만드는 버튼을 보여준다. 이 단계까지 구현하면 여러 브라우저 탭에서 편집 내용을 동기화해 볼 수 있지만, 예시의 ID가 하드코딩되어 있으므로 실제 제품 데이터와 연결하는 작업이 이어진다.
7. 문서 ID와 서버 측 생성의 의미
각 동기화 문서는 하나의 ID와 연결되며, 보통 기존 제품 테이블의 문서 ID를 그대로 사용할 수 있다. 예를 들어 작업 테이블의 설명 필드를 협업 문서로 만들고 싶다면 해당 task의 ID를 협업 문서 ID로 삼을 수 있다. 클라이언트 측 생성은 오프라인이거나 네트워크가 불안정한 상황에서도 문서를 만들 수 있다는 장점이 있지만, 서버 측 생성은 관련 데이터를 하나의 트랜잭션 안에서 만들고 초기 문서 내용을 일관되게 설정할 수 있다는 장점이 있다. 본문은 서버 mutation에서 task를 insert한 뒤 같은 ID로 prosemirrorSync.create를 호출하면, 이후 UI에서 task가 존재할 때 문서도 항상 존재한다고 기대할 수 있다고 설명한다.
8. 권한 검증과 스냅샷 활용
협업 문서는 누가 읽고 쓸 수 있는지를 서버에서 반드시 검증해야 한다. 이를 위해 syncApi 정의에 checkRead와 checkWrite 콜백을 추가하고, ctx.getUserIdentity 또는 Convex Auth의 getAuthUserId 같은 도구로 현재 로그인한 사용자를 확인할 수 있다. 예시로는 사용자가 해당 task와 연결된 팀에 속해 있는지, 관리자 권한이 있는지, task가 편집 가능한 상태인지, 로그아웃 사용자에게 공개되어도 되는 문서인지 등을 확인할 수 있다고 제시한다. 또한 편집 중에는 작은 변경분이 즉시 전송되고, 편집이 잠시 멈추면 현재 문서의 스냅샷이 저장되며, 이 스냅샷은 새 사용자나 새 탭이 모든 변경 이력을 다시 받지 않고 최신 상태를 가져오게 해 준다. 스냅샷은 문서의 텍스트 버전을 별도 필드에 저장해 전체 텍스트 검색에 쓰거나, 콘텐츠 기반 벡터 검색을 위한 임베딩 생성 같은 후속 처리에도 활용될 수 있다.
🧾 핵심 주장 / 시사점
- 협업 편집에서 핵심 문제는 저장 빈도가 아니라 충돌 모델이다. 전체 문자열을 계속 덮어쓰는 방식은 빠르게 저장하더라도 동시 사용자의 의도를 안전하게 병합하지 못한다.
- 편집기 선택은 UI 취향만의 문제가 아니라 장기 데이터 모델 선택이다. Tiptap과 BlockNote가 모두 ProseMirror 계열이어도 저장 구조가 달라, 공개 후 전환에는 데이터 마이그레이션 부담이 생길 수 있다.
- 스냅샷은 단순 백업이 아니라 제품 기능의 연결 지점이다. 증분 변경은 실시간 협업을 담당하고, 스냅샷은 검색·파생 데이터·외부 시스템 업데이트처럼 약간 지연되어도 되는 기능을 효율적으로 지원한다.
✅ 액션 아이템
- 다중 사용자 충돌을 줄이기 위해 공유 textarea를 즉시 교체하고 ProseMirror 기반 동기화 편집기로 전환한다.
- 문서 ID를 기존 테이블의 ID 또는 UUID로 두고 Convex와 연계해 생성·조회·저장 경로의 모델을 정합성 있게 통합한다.
- 접근 제어를 checkRead/checkWrite로 일괄 적용하고, 편집 스냅샷을 텍스트·벡터 검색용 파생 데이터 생성 흐름에 연결한다.
❓ 열린 질문
- Tiptap과 BlockNote 중 공개 출시 전 실제 데이터 모델 충돌 위험이 낮은 선택지는 무엇인가?
- 문서 생성은 클라이언트 경로와 서버 경로 중 어떤 조건에서 분기해야 제품 데이터 일관성이 유지되는가?
- 편집 중 생성되는 스냅샷을 텍스트 검색과 벡터 검색 파생 데이터에 반영할 때 갱신 주기는 어떤 기준으로 정해야 하는가?