다음은 Editor.tsx
컴포넌트의 변경사항입니다.
CRDT기반
의 연결리스트 구조로 수정했습니다.
LinkedList
를 직접 관리하는 방식으로 변경했습니다.BlockId
속성을 관리하도록 수정했습니다.clock
속성을 추가해 동시성을 제어합니다.// 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,
});
localInsert()
)/삭제(localDelete()
) 로직으로 변경했습니다.
head
에 추가, currentCaret 1
증가1
증가1
증가해당 블록 삭제
1
감소// 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;
}