[]
이 튜토리얼은 튜토리얼: 실시간 동시 작업 텍스트 편집기를 기반으로 하여 Presence 기능을 추가합니다. Presence는 온라인 사용자와 그들의 커서 위치를 표시하는 데 사용됩니다. 사용자는 에디터 내에서 다른 사용자들의 실시간 상태(예: 사용자 이름, 선택 영역 위치)를 확인할 수 있어 동시 작업 경험이 향상됩니다.
한 사용자가 에디터에 문자를 입력하면, 다른 사용자들은 커서 위치, 선택 영역, 사용자 이름, 입력 내용을 실시간으로 확인할 수 있습니다.

JavaScript 및 HTML/CSS 기본 지식
프로젝트 루트 디렉터리에서 다음 명령어를 실행하여 Presence 관련 npm 패키지를 설치합니다.
npm install @mescius/js-collaboration-presence @mescius/js-collaboration-presence-client
npm install quill-cursorsserver.js를 수정하여 Presence 기능을 추가합니다.
import express from "express";
import { createServer } from "http";
import { Server } from "@mescius/js-collaboration";
import OT from "@mescius/js-collaboration-ot";
import { presenceFeature } from "@mescius/js-collaboration-presence";
import richText from "rich-text";
const app = express();
const httpServer = createServer(app);
const server = new Server({ httpServer });
// rich-text 타입 등록
OT.TypesManager.register(richText.type);
// OT 문서 서비스 초기화
server.useFeature(OT.documentFeature());
// presence 서비스 초기화
server.useFeature(presenceFeature());
// 정적 파일 제공
app.use(express.static("public"));
// 서버 시작
httpServer.listen(8080, () => {
console.log("Server running at http://localhost:8080");
});온라인 사용자 커서 표시 기능을 추가하기 위해 클라이언트 파일을 수정합니다.
public/client.js를 업데이트합니다.
import { Client } from "@mescius/js-collaboration-client";
import * as OT from "@mescius/js-collaboration-ot-client";
import { Presence } from "@mescius/js-collaboration-presence-client";
import richText from "rich-text";
import Quill from "quill";
import QuillCursors from "quill-cursors";
Quill.register('modules/cursors', QuillCursors);
// rich-text 타입 등록
OT.TypesManager.register(richText.type);
// 서버에 연결하고 룸에 참여
const connection = new Client().connect("room-id");
const doc = new OT.SharedDoc(connection);
const quill = new Quill("#editor", { theme: "bubble", modules: { cursors: true } });
const cursors = quill.getModule('cursors');
// Presence 초기화
const presence = new Presence(connection);
const userId = `user-${Math.random().toString(36).substring(2, 9)}`; // 랜덤 사용자 ID 생성
const DEFAULT_COLOR_SCHEME = ['#0000ff', '#008000', '#9900cc', '#800000', '#00cc33', '#cc6600', '#cc0099'];
// 문서 구독
doc.subscribe().then(async () => {
if (!doc.type) {
try {
await doc.create([{ insert: "Hi!" }], richText.type.uri, {});
} catch (err) {
console.error("Create Document Error:", err);
}
}
quill.setContents(doc.data);
quill.on("text-change", (delta, oldDelta, source) => {
if (source !== "user") return;
doc.submitOp(delta, { source: connection.id });
});
doc.on("op", (op, source) => {
if (source === connection.id) return;
quill.updateContents(op);
// 콘텐츠 업데이트는 선택 영역 변경을 유발하지만,
// Quill은 selection-change 이벤트를 트리거하지 않음
presence.submitLocalStateField('selection', quill.getSelection());
});
});
// 원격 프레즌스 표시
function updatePresenceList() {
cursors.clearCursors();
for (const id in presence.otherStates) {
const data = presence.otherStates[id];
cursors.createCursor(id, data.userId, data.color);
cursors.moveCursor(id, data.selection);
}
}
// Presence 업데이트 구독
presence.subscribe().then(() => {
updatePresenceList();
let color = DEFAULT_COLOR_SCHEME[Object.keys(presence.otherStates).length % DEFAULT_COLOR_SCHEME.length];
presence.submitLocalState({ userId, selection: quill.getSelection(), color });
// 선택 영역 변경 시 로컬 프레즌스 업데이트
quill.on("selection-change", (range) => {
presence.submitLocalStateField('selection', range);
});
presence.on("add", () => updatePresenceList());
presence.on("update", () => updatePresenceList());
presence.on("remove", () => updatePresenceList());
})
// 오류 처리
connection.on("error", (err) => console.error("Connection error:", err.message));
doc.on("error", (err) => console.error("Document error:", err.message));클라이언트 코드 번들링
npm run build서버 시작
npm run start다음 출력이 표시되어야 합니다: Server running at http://localhost:8080.
기능 테스트
브라우저를 열고 http://localhost:8080에 접속합니다.
동일한 주소를 여러 창에서 열고 내용을 편집하여 실시간 동기화를 확인합니다.