← 활용 사례

협업: 누가 있는지, 실시간 커서, 편집 잠금

같은 문서나 티켓을 보고 있는 모든 사람의 아바타를 보여 주고, 커서와 포커스를 공유하며, 저장된 변경을 전파해 누구도 다른 사람의 작업을 덮어쓰지 않게 합니다.

두 사람이 모르고 같은 레코드를 편집하면 한 사람의 작업이 사라집니다. 같은 문서, 티켓, 디자인을 누가 보고 있는지, 어디에 있는지 보여 주면 이를 막을 수 있고, 제품에 함께 쓰고 있다는 느낌도 생깁니다. 프로젝트 관리 도구, CRM, 헬프데스크, 화이트보드, 폼 빌더 모두에 도움이 됩니다.

사용자가 얻는 것

  • 같은 페이지를 보고 있는 모든 사람의 아바타. 사람이 오가면 갱신됩니다.
  • 실시간 커서, 또는 누군가 편집 중인 필드의 강조 표시.
  • 저장 충돌 대신 분명한 “Ann 님이 이 티켓을 편집 중” 잠금 표시.
  • 다른 사람의 변경이 새로고침 없이 그 자리에 나타납니다.

사용하는 기능

  • 프레즌스 채널(presence-): 문서마다 하나, 멤버 목록은 모든 노드에서 정확하게 유지됩니다.
  • 클라이언트 이벤트(client-*): 커서 위치와 포커스를 브라우저끼리 주고받습니다.
  • 서버 이벤트: 서버가 변경을 저장한 뒤에 발행합니다.
  • 대형 프레즌스 채널(presence-large-): 동시에 100명이 넘게 볼 수 있는 페이지에.

1단계: 누가 있는지 보여 주기

presence-doc-<id>는 문서 자체의 접근 권한을 확인하는 것과 같은 방식으로 인가하고, user_info로 이름과 아바타를 반환합니다(라우트는 채팅과 입력 중 표시 참고). 그다음:

const doc = pusher.subscribe('presence-doc-' + docId);

doc.bind('pusher:subscription_succeeded', (members) => avatars.render(members));
doc.bind('pusher:member_added', () => avatars.render(doc.members));
doc.bind('pusher:member_removed', (m) => {
  avatars.render(doc.members);
  cursors.remove(m.id);
});

두 탭에서 연 사용자는 한 명으로 셉니다. 첫 소켓으로 추가되고 마지막 소켓이 닫힐 때 제거됩니다.

2단계: 커서와 포커스 공유

앱 설정에서 클라이언트 이벤트 허용을 켭니다. 위치는 바뀔 때만, 1초에 몇 번까지만 보냅니다.

let pending = null;
document.addEventListener('pointermove', (e) => {
  pending = { x: e.pageX / document.body.scrollWidth, y: e.pageY };
});
setInterval(() => {
  if (!pending) return;
  doc.trigger('client-cursor', pending);
  pending = null;
}, 200);

doc.bind('client-cursor', (pos, meta) => cursors.move(meta.user_id, pos));

// Field locks: who is editing what
field.addEventListener('focus', () => doc.trigger('client-focus', { field: field.name }));
doc.bind('client-focus', ({ field }, meta) => locks.show(field, doc.members.get(meta.user_id)));

프레즌스 채널에서는 클라이언트 이벤트 핸들러의 두 번째 인자에 보낸 사람의 user_id가 들어 있습니다.

3단계: 저장된 변경 전파

누군가 저장하면 변경을 저장한 뒤 다른 사람들에게 발행합니다.

app.patch('/docs/:id', requireLogin, async (req, res) => {
  const change = await db.docs.apply(req.params.id, req.user.id, req.body.patch);
  await pusher.trigger('presence-doc-' + req.params.id, 'changed', { version: change.version, patch: req.body.patch, by: req.user.id }, { socket_id: req.body.socket_id });
  res.json(change);
});

다른 시청자는 패치를 적용하거나, 자기 버전이 뒤처졌으면 레코드를 다시 불러옵니다.

100명이 넘는 시청자

프레즌스 채널은 모두가 전체 목록을 받기 때문에 최대 100명까지입니다. 전사 공지나 라이브 이벤트처럼 많은 사람이 보는 페이지에는 presence-large-doc-<id>와 @hibira-io/large-presence를 쓰세요. 최대 100,000명까지 들어가며, 입장마다 이벤트를 보내는 대신 인원수와 샘플을 보냅니다. 여기서는 클라이언트 이벤트를 쓸 수 없으니 커서 공유는 작은 방에서만 하세요.

팁

  • 클라이언트 이벤트는 하나하나가 메시지 1건입니다. 편집자 10명이 1초에 5번씩 커서를 보내면 초당 50건이므로, 바뀔 때만 보내고 탭이 숨겨지면 빈도를 낮추세요.
  • 기준 데이터는 서버에 두세요. 클라이언트 이벤트는 커서와 포커스 같은 힌트용이며, 저장되지 않고 보낸 사람에게 돌아가지도 않습니다.
  • 본격적인 공동 텍스트 편집에는 Yjs나 Automerge 같은 CRDT 라이브러리의 연산을 같은 방식으로 서버를 거쳐 보내세요.