실시간 협업
@inkio/collab는 Yjs 기반 실시간 협업을 Inkio 에디터에 연결하는 패키지입니다.
전송 계층은 Hocuspocus 를 씁니다.
표준 Yjs sync 프로토콜이라 재접속 때 차이만 주고받고, presence·인증·읽기 전용·저장은
서버가 책임집니다. 서버는 examples/collab-server 참조 구현에서 출발하세요.
설치
pnpm add @inkio/collab yjsyjs·@hocuspocus/*·y-indexeddb는 external이라 호스트 앱과 단일 복사본을 공유합니다.
방 만들기
Next.js App Router에서는 에디터가 클라이언트 컴포넌트 안에 있어야 합니다.
'use client';
// EditorContent는 @inkio/collab이 재노출합니다. @tiptap/react를 따로 설치하지 마세요.
import { useInkioCollaborativeEditor, CollabPresence, EditorContent } from '@inkio/collab';
export function Room({ docId, token }: { docId: string; token: string }) {
const { editor, provider, status, readOnly } = useInkioCollaborativeEditor({
docId,
url: 'ws://localhost:3123',
token,
user: { name: 'Ada', color: '#ff0077' },
content: '<p>Start here</p>',
offline: true,
});
if (!editor) return null;
return (
<>
<span>{status}{readOnly ? ' (읽기 전용)' : ''}</span>
<CollabPresence provider={provider} />
<EditorContent editor={editor} />
</>
);
}훅이 대신 처리하는 규칙:
- 협업 모드에서는
history/undoRedo확장을 제거합니다 (Yjs undo와 충돌).extensions를 생략하면 core 기본값을, 빈 배열이면 최소 문서 스키마를 씁니다. - 원격 사용자의 캐럿과 선택 영역을 에디터 안에 그립니다 (별도 CSS 불필요).
content는 동기화 후 문서가 비어 있을 때만 심는 시드입니다. 시드는 결정적(deterministic) Yjs 업데이트로 만들어져서, 여러 사람이 동시에 빈 방에 들어와도 한 벌로 합쳐집니다. 모든 클라이언트가 같은 시드를 넘기면 됩니다.token은 연결·재연결 때마다 다시 읽습니다. 문자열이 바뀌어도 에디터를 다시 만들지 않고, 함수를 넘기면 매번 새 토큰을 받아옵니다.docId·url이 바뀌면 provider와 에디터를 새로 만듭니다.- 서버가 읽기 전용 권한을 주면
readOnly가true가 되고 에디터가 편집 불가로 바뀝니다. offline: true면 문서를 IndexedDB에 캐시해 오프라인 편집이 새로고침 후에도 남고, 재접속 때 서버와 합쳐집니다.- React StrictMode의 이중 마운트에서도 안전합니다 (provider는 effect 안에서 생성·정리).
status는 connecting → synced, 끊기면 disconnected, 인증 거부 시 unauthorized입니다.
여러 문서를 한 소켓으로
페이지 트리처럼 문서를 여러 개 여는 앱은 소켓 하나를 공유하세요.
import { HocuspocusProviderWebsocket, useInkioCollaborativeEditor } from '@inkio/collab';
const socket = new HocuspocusProviderWebsocket({ url: 'ws://localhost:3123' });
// 각 문서 컴포넌트에서
useInkioCollaborativeEditor({ docId, websocketProvider: socket });서버
examples/collab-server는 Hocuspocus 참조 구현입니다 (배포하지 않음).
단독 서버(createInkioCollabServer)로도, NestJS 앱에 임베드(InkioCollabModule)해서도 띄울 수 있습니다.
- 문서마다
<dataDir>/<docId>.ydoc스냅샷을 디바운스 저장하고, 방에 처음 들어올 때 불러옵니다. 아무도 없는 문서는 메모리에서 내립니다. verify(token, docId)가'write'면 편집,'read'면 서버가 쓰기를 거부하는 읽기 전용,false면 연결을 거부합니다.- presence는 서버가 방 단위로 관리해서, 늦게 들어온 사람도 기존 참가자를 바로 보고 나간 사람은 즉시 사라집니다.
pnpm --filter example-collab-server build
pnpm --filter example-collab-server start # ws://localhost:3123/collab (PORT, DATA_DIR)
pnpm --filter example-collab-server smoke # 단독 + Nest 모드 동기화·인증·복원 확인NestJS에 임베드
Nest 앱과 같은 HTTP 서버·포트를 쓰고, WebSocket은 path(기본 /collab)로만 받습니다.
다른 라우트와 충돌하지 않습니다.
@Module({
imports: [
InkioCollabModule.forRoot({
dataDir: './data',
verify: (token, docId) => access.check(token, docId), // 'write' | 'read' | false
}),
],
})
export class AppModule {}
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // 종료 시 대기 중인 저장을 flush
await app.listen(3123);클라이언트는 url: 'ws://localhost:3123/collab'로 접속합니다. 서비스에서 문서를 직접
다루려면 INKIO_HOCUSPOCUS를 주입받아 openDirectConnection(docId)를 쓰세요.
Nest 밖의 Express/Fastify 서버에는 attachHocuspocus(httpServer, hocuspocus)를 쓰면 됩니다.
인증 걸기
createInkioCollabServer({
port: 3123,
dataDir: './data',
verify: async (token, docId) => {
const role = await lookupRole(token, docId);
return role === 'editor' ? 'write' : role === 'viewer' ? 'read' : false;
},
});저장소를 파일 대신 DB로 바꾸려면 @hocuspocus/extension-database의 fetch/store를
교체하세요. 서버를 여러 대 띄우면 @hocuspocus/extension-redis로 묶습니다.
서버에서 시드 넣기
HTML 시드는 DOM이 필요해 브라우저에서만 파싱됩니다. 서버에서 문서를 미리 채우려면
JSON과 에디터 스키마로 createSeedUpdate(schema, json)를 만들어 적용하세요.
오프라인 저장 직접 다루기
훅의 offline 옵션 대신 직접 관리할 수도 있습니다.
import { persistDocToIndexedDB } from '@inkio/collab';
const persistence = persistDocToIndexedDB(docId, doc);
// ...
persistence.destroy(); // 언마운트 시 반드시 호출 (IDB 연결 누수 방지)하나의
docId에 live persistence를 둘 이상 열지 마세요. 스냅샷을 한 번만 읽고 끝내려면 타임아웃 내장된loadIndexedDBState(docId, doc)를 쓰세요.
Presence
awareness 상태는 { user: { name, color } }를 싣습니다. CollabPresence가 접속자 아바타를
그리고, useCollabPeers(provider)로 목록을 직접 받을 수도 있습니다.