다음은 Editor.tsx 컴포넌트의 변경사항입니다.

CRDT와 연동

Editor.tsx

상태 관리 구조 변경

// before
const [editorList] = useState(() => new LinkedListBlock());
const [editorState, setEditorState] = useState<EditorState>(() => ({
  rootNode: editorList.root,
  currentNode: editorList.current,
}));
const [isComposing, setIsComposing] = useState(false);

// after
const editorCRDT = useRef<EditorCRDT>(new EditorCRDT(0));
const [editorState, setEditorState] = useState<EditorStateProps>({
  clock: editorCRDT.current.clock,
  linkedList: editorCRDT.current.LinkedList,
  currentBlock: null as BlockId | null,
});

블록 조작 로직 변경

// before
const handleInput = useCallback(() => {
  if (isComposing) return;

  if (contentRef.current && editorState.currentNode) {
    const newContent = contentRef.current.textContent || "";
    editorState.currentNode.content = newContent;
  }
}, [editorState.currentNode, isComposing]);

// after
const handleBlockInput = useCallback(
    (e: React.FormEvent<HTMLDivElement>, blockId: BlockId) => {
      const block = editorState.linkedList.getNode(blockId);
      if (!block) return;

      const element = e.currentTarget;
      const newContent = element.textContent || "";
      const currentContent = block.crdt.read();
      const selection = window.getSelection();
      const caretPosition = selection?.focusOffset || 0;

      if (newContent.length > currentContent.length) {
        // 텍스트 추가 로직
        if (caretPosition === 0) {
          const [addedChar] = newContent;
          block.crdt.localInsert(0, addedChar);
          block.crdt.currentCaret = 1;
        } else if (caretPosition > currentContent.length) {
          const addedChar = newContent[newContent.length - 1];
          block.crdt.localInsert(currentContent.length, addedChar);
          block.crdt.currentCaret = caretPosition;
        } else {
          const addedChar = newContent[caretPosition - 1];
          block.crdt.localInsert(caretPosition - 1, addedChar);
          block.crdt.currentCaret = caretPosition;
        }
      } else if (newContent.length < currentContent.length) {
        // 텍스트 삭제 로직
        if (caretPosition === 0) {
          block.crdt.localDelete(0);
          block.crdt.currentCaret = 0;
        } else {
          block.crdt.localDelete(caretPosition);
          block.crdt.currentCaret = caretPosition;
        }
      }

      setEditorState((prev) => ({
        clock: editorCRDT.current.clock,
        linkedList: editorCRDT.current.LinkedList,
        currentBlock: prev.currentBlock,
      }));
    },
    [editorState.linkedList],
  );

키입력 로직 수정

기존 연결리스트 구조에서는 서버와 통신하는 로직이 없기 때문에 CRDT 구조에 맞게 데이터를 가공해 서버로 전송하는 로직을 추가했습니다. 엔터 입력이 들어왔을 경우,

상태 동기화 로직

Socket.io를 통한 실시간 이벤트 전파

기존에는 로컬 상태를 직접 변경했지만, 변경된 코드에서는 localUpdate() 메서드를 통해 해당 블록의 타입을 변경해 클라이언트 CRDT 인스턴스에 반영합니다. CRDT의 local연산의 경우 operation이라는 객체를 리턴합니다.

Operation: noctaCRDT 라이브러리에서 정의하는 클라이언트와 서버간 통신하는 데이터 구조

localUpdate()메서드가 리턴하는 operation은 다음과 같습니다.

export interface RemoteBlockUpdateOperation {
  node: Block;
  pageId: string;
}