Skip to content

실시간 편집 구현 과정

진예원 edited this page Dec 3, 2024 · 1 revision

YJS란?

yjs

Yjs는 자동으로 동기화되는 협업 애플리케이션을 구축하기 위한 고성능 CRDT 라이브러리이다.

내부 CRDT 모델을 동시에 조작할 수 있는 공유 **데이터 타입을 제공한다.

공유 타입(Shared Types)은 Array, Map 또는 Set과 같은 일반 데이터 유형과 유사하다.

자동으로 상태를 동기화하고, 변경이 발생하면 이벤트를 발생시키고, 병합 충돌 없이 자동으로 병합할 수 있다.


YJS를 이용하여 다른 피어들과 동기화하는 방법

네트워크 공급자(e.g. WebSocket, WebRTC)에 YDoc을 전달하여 문서 업데이트를 다른 피어에 동기화하면 된다.

네트워크 공급자는 문서 업데이트를 다른 피어에 동기화한다.

// 클라이언트
import { SocketIOProvider } from "y-socket.io";
import * as Y from "yjs";

export const createSocketIOProvider = (roomname: string, ydoc: Y.Doc) => {
  return new SocketIOProvider(
    import.meta.env.VITE_WS_URL,
    roomname,
    ydoc,
    {
      autoConnect: true,
      disableBc: false,
    },
    {
      reconnectionDelayMax: 10000,
      timeout: 5000,
      transports: ["websocket", "polling"],
    },
  );
};

// 서버
this.ysocketio = new YSocketIO(this.server, {
  gcEnabled: true,
});

this.ysocketio.initialize();

this.ysocketio.on('document-loaded', async (doc: Y.Doc) => {
	// 피어 간에 동기화되는 ydoc을 서버에서도 관리할 수 있다.
	...
}

YJS를 사용한 이유

우리 프로젝트 목표는 라이브러리를 적극 사용해서 빠르게 배포하고, 유저 QA로 서비스를 발전시켜 완성도 높은 서비스를 만드는 것이었다.

이를 위해, 노드와 엣지 캔버스를 위해 React Flow와 노션과 비슷한 강력한 기능을 제공하는 에디터를 위해 Novel을 이용했는데, 이 두 라이브러리와 잘 맞고 …



프로젝트 적용 과정

초기 설정

Provider에 YDoc을 설정하고, 웹소켓 서버에 연결한다. → 피어 간 동기화!!

const { ydoc } = useYDocStore(); // 전역으로 관리되는 YDoc
const wsProvider = createSocketIOProvider(`flow-room-${workspace}`, ydoc); // Provider

프로젝트에서 사용한 공유 데이터 타입

Y.Text : 에디터의 제목, 이모지

Y.Map : 노드와 엣지

Y.XmlFragment : 에디터 컨텐츠


노드(페이지) 생성

피어 간 공유되는 Y.Map 에 새로운 노드를 추가하고, 이 Y.Map 을 React Flow가 제공하는 useNodesState 훅으로 캔버스에 렌더링한다.

Y.Map에 노드 생성 후 추가하기

createMutation
  .mutateAsync(...)
  .then((res) => {
    const nodesMap = ydoc.getMap("nodes");
    nodesMap.set(res.pageId.toString(), {
      id: res.pageId.toString(),
      type: "note",
      data: {
        title: "제목 없음",
        id: res.pageId,
        emoji: "",
      },
      position: { x: 0, y: 0 },
      selected: false,
      isHolding: false,
    });
})
  1. 서버에 POST 요청하고 생성된 노드id를 response로 받는다
  2. nodesMap 에 노드에 대한 정보를 추가한다.

Y.Map와 useNodesState를 이용한 노드 렌더링

Y.Mapobserve 메서드로 피어 간 공유되는 Y.Map의 변경을 감지할 수 있다.

https://docs.yjs.dev/api/shared-types/y.map#observing-changes-y.mapevent

https://docs.yjs.dev/api/y.event

react flow가 제공하는 useNodesState 훅으로 캔버스에 노드들을 렌더링을 할 수 있다.

const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
nodesMap.observe((event) => {
  ...
  event.changes.keys.forEach((change, key) => {
    const nodeId = key;
    if (change.action === "add") {
      const updatedYNode = nodesMap.get(nodeId) as YNode;
      const updatedNode = Object.fromEntries(updatedNodeEntries) as Node;
      
      queryClient.invalidateQueries({ queryKey: ["pages"] }); // ??

      setNodes((nds) => {
        const index = nds.findIndex((n) => n.id === nodeId);
        if (index === -1) {
          return [...nds, updatedNode];
        }
        const newNodes = [...nds];
        newNodes[index] = {
          ...updatedNode,
          selected: newNodes[index].selected,
        };
        return newNodes;
      });
    }
  });
});

노드 위치 변경

react flow의 캔버스가 제공하는 onNodesChange 이벤트 핸들러로 노드 상태 변경을 감지하고, 이를 Y.Map에 반영한다.

const handleNodesChange = useCallback(
  (changes: NodeChange[]) => {
    if (!ydoc) return;
    const nodesMap = ydoc.getMap("nodes");
    const edgesMap = ydoc.getMap("edges");

    changes.forEach((change) => {
      if (change.type === "position" && change.position) {
        const node = nodes.find((n) => n.id === change.id);
        if (node) {
          const updatedYNode: YNode = {
            ...node,
            position: change.position,
            selected: false,
            isHolding: holdingNodeRef.current === change.id,
          };
          nodesMap.set(change.id, updatedYNode);
        }
      }
    });
    onNodesChange(changes);
  },
  [nodes, edges, onNodesChange],
);

개발 문서

⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
🚧 트러블슈팅

팀 문화

🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략

그룹 기록

📢 발표 자료
🌤️ 데일리 스크럼
📑 회의록
🏖️ 그룹 회고
🚸 멘토링 일지
Clone this wiki locally