Skip to content

Commit 48310db

Browse files
Merge pull request #15 from boostcampwm-2024/refactor/#2-리스트-가상화
Refactor/#2 리스트 가상화
2 parents 4cf6f49 + 2c87c69 commit 48310db

File tree

8 files changed

+133
-79
lines changed

8 files changed

+133
-79
lines changed

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@noctaCrdt": "workspace:*",
1919
"@pandabox/panda-plugins": "^0.0.8",
2020
"@tanstack/react-query": "^5.60.5",
21+
"@tanstack/react-virtual": "^3.11.2",
2122
"axios": "^1.7.7",
2223
"framer-motion": "^11.11.11",
2324
"react": "^18.3.1",

client/src/features/editor/Editor.style.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,6 @@ export const editorContainer = css({
1212
},
1313
});
1414

15-
export const editorTitleContainer = css({
16-
display: "flex",
17-
gap: "4px",
18-
flexDirection: "column",
19-
width: "full",
20-
padding: "spacing.sm",
21-
});
22-
2315
export const editorTitle = css({
2416
textStyle: "display-medium28",
2517
outline: "none",

client/src/features/editor/Editor.tsx

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,11 @@ import { EditorCRDT } from "@noctaCrdt/Crdt";
44
import { BlockLinkedList } from "@noctaCrdt/LinkedList";
55
import { Block as CRDTBlock } from "@noctaCrdt/Node";
66
import { serializedEditorDataProps } from "@noctaCrdt/types/Interfaces";
7+
import { useVirtualizer } from "@tanstack/react-virtual";
78
import { useRef, useState, useCallback, useEffect, useMemo, memo } from "react";
89
import { useSocketStore } from "@src/stores/useSocketStore.ts";
910
import { setCaretPosition, getAbsoluteCaretPosition } from "@src/utils/caretUtils.ts";
10-
import {
11-
editorContainer,
12-
editorTitleContainer,
13-
editorTitle,
14-
addNewBlockButton,
15-
} from "./Editor.style";
11+
import { editorContainer, editorTitle, addNewBlockButton } from "./Editor.style";
1612
import { Block } from "./components/block/Block";
1713
import { useBlockDragAndDrop } from "./hooks/useBlockDragAndDrop";
1814
import { useBlockOperation } from "./hooks/useBlockOperation.ts";
@@ -50,7 +46,6 @@ export const Editor = memo(
5046
const { clientId } = useSocketStore();
5147
const [displayTitle, setDisplayTitle] = useState(pageTitle);
5248
const [dragBlockList, setDragBlockList] = useState<string[]>([]);
53-
console.log(serializedEditorData);
5449

5550
useEffect(() => {
5651
if (pageTitle === "새로운 페이지" || pageTitle === "") {
@@ -273,7 +268,35 @@ export const Editor = memo(
273268
if (isLocalChange.current || isSameLocalChange.current) {
274269
setCaretPosition({
275270
blockId: editorCRDT.current.currentBlock!.id,
276-
linkedList: editorCRDT.current.LinkedList,
271+
position: editorCRDT.current.currentBlock?.crdt.currentCaret,
272+
pageId,
273+
});
274+
isLocalChange.current = false;
275+
isSameLocalChange.current = false;
276+
return;
277+
}
278+
}, [editorCRDT.current.currentBlock?.id.serialize()]);
279+
280+
// 리스트 가상화
281+
const editorRef = useRef<HTMLDivElement>(null);
282+
283+
const virtualizer = useVirtualizer({
284+
count: editorState.linkedList.spread().length,
285+
getScrollElement: () => editorRef.current,
286+
estimateSize: () => 24,
287+
overscan: 5,
288+
});
289+
290+
useEffect(() => {
291+
if (!editorCRDT || !editorCRDT.current.currentBlock) return;
292+
293+
const { activeElement } = document;
294+
if (activeElement?.tagName.toLowerCase() === "input") {
295+
return; // input에 포커스가 있으면 캐럿 위치 변경하지 않음
296+
}
297+
if (isLocalChange.current || isSameLocalChange.current) {
298+
setCaretPosition({
299+
blockId: editorCRDT.current.currentBlock!.id,
277300
position: editorCRDT.current.currentBlock?.crdt.currentCaret,
278301
pageId,
279302
});
@@ -352,18 +375,23 @@ export const Editor = memo(
352375
return <div>Loading editor data...</div>;
353376
}
354377
return (
355-
<div data-testid={`editor-${testKey}`} className={editorContainer}>
356-
<div className={editorTitleContainer}>
357-
<input
358-
data-testid={`editorTitle-${testKey}`}
359-
type="text"
360-
placeholder="제목을 입력하세요..."
361-
onChange={handleTitleChange}
362-
onBlur={handleBlur}
363-
value={displayTitle}
364-
className={editorTitle}
365-
/>
366-
<div style={{ height: "36px" }}></div>
378+
<div data-testid={`editor-${testKey}`} className={editorContainer} ref={editorRef}>
379+
<input
380+
data-testid={`editorTitle-${testKey}`}
381+
type="text"
382+
placeholder="제목을 입력하세요..."
383+
onChange={handleTitleChange}
384+
onBlur={handleBlur}
385+
value={displayTitle}
386+
className={editorTitle}
387+
/>
388+
<div style={{ height: "36px" }}></div>
389+
<div
390+
style={{
391+
height: virtualizer.getTotalSize(),
392+
position: "relative",
393+
}}
394+
>
367395
<DndContext
368396
onDragEnd={(event: DragEndEvent) => {
369397
handleDragEnd(event, dragBlockList, () => setDragBlockList([]));
@@ -379,37 +407,43 @@ export const Editor = memo(
379407
.map((block) => `${block.id.client}-${block.id.clock}`)}
380408
strategy={verticalListSortingStrategy}
381409
>
382-
{editorState.linkedList.spread().map((block, idx) => (
383-
<Block
384-
testKey={`block-${idx}`}
385-
key={`${block.id.client}-${block.id.clock}`}
386-
id={`${block.id.client}-${block.id.clock}`}
387-
block={block}
388-
isActive={block.id === editorCRDT.current.currentBlock?.id}
389-
onInput={handleBlockInput}
390-
onCompositionStart={handleCompositionStart}
391-
onCompositionUpdate={handleCompositionUpdate}
392-
onCompositionEnd={handleCompositionEnd}
393-
onKeyDown={handleKeyDown}
394-
onCopy={handleCopy}
395-
onPaste={handlePaste}
396-
onClick={handleBlockClick}
397-
onAnimationSelect={handleAnimationSelect}
398-
onTypeSelect={handleTypeSelect}
399-
onCopySelect={handleCopySelect}
400-
onDeleteSelect={handleDeleteSelect}
401-
onTextStyleUpdate={onTextStyleUpdate}
402-
onTextColorUpdate={onTextColorUpdate}
403-
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
404-
dragBlockList={dragBlockList}
405-
onCheckboxToggle={handleCheckboxToggle}
406-
/>
407-
))}
410+
{virtualizer.getVirtualItems().map((virtualRow) => {
411+
const block = editorState.linkedList.spread()[virtualRow.index];
412+
return (
413+
<Block
414+
testKey={`block-${virtualRow.index}`}
415+
virtualStart={virtualRow.start}
416+
virtualIndex={virtualRow.index}
417+
virtualRef={virtualizer.measureElement}
418+
key={`${block.id.client}-${block.id.clock}`}
419+
id={`${block.id.client}-${block.id.clock}`}
420+
block={block}
421+
isActive={block.id === editorCRDT.current.currentBlock?.id}
422+
onInput={handleBlockInput}
423+
onCompositionStart={handleCompositionStart}
424+
onCompositionUpdate={handleCompositionUpdate}
425+
onCompositionEnd={handleCompositionEnd}
426+
onKeyDown={handleKeyDown}
427+
onCopy={handleCopy}
428+
onPaste={handlePaste}
429+
onClick={handleBlockClick}
430+
onAnimationSelect={handleAnimationSelect}
431+
onTypeSelect={handleTypeSelect}
432+
onCopySelect={handleCopySelect}
433+
onDeleteSelect={handleDeleteSelect}
434+
onTextStyleUpdate={onTextStyleUpdate}
435+
onTextColorUpdate={onTextColorUpdate}
436+
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
437+
dragBlockList={dragBlockList}
438+
onCheckboxToggle={handleCheckboxToggle}
439+
/>
440+
);
441+
})}
408442
</SortableContext>
409443
</DndContext>
410444
{editorState.linkedList.spread().length === 0 && (
411445
<div
412-
data-testId="addNewBlockButton"
446+
data-testid="addNewBlockButton"
413447
className={addNewBlockButton}
414448
onClick={addNewBlock}
415449
>

client/src/features/editor/components/block/Block.style.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const baseBlockStyle = {
44
display: "flex",
55
flexDirection: "row",
66
alignItems: "center",
7-
position: "relative",
87
width: "full",
98
minHeight: "16px",
109
backgroundColor: "transparent",

client/src/features/editor/components/block/Block.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import {
2727
} from "./Block.style";
2828

2929
interface BlockProps {
30+
virtualIndex: number;
31+
virtualStart: number;
32+
virtualRef: (node: Element | null | undefined) => void;
3033
testKey: string;
3134
id: string;
3235
block: CRDTBlock;
@@ -71,6 +74,9 @@ interface BlockProps {
7174
}
7275
export const Block: React.FC<BlockProps> = memo(
7376
({
77+
virtualIndex,
78+
virtualRef,
79+
virtualStart,
7480
testKey,
7581
id,
7682
block,
@@ -280,7 +286,6 @@ export const Block: React.FC<BlockProps> = memo(
280286

281287
useEffect(() => {
282288
if (blockRef.current) {
283-
console.log(block.crdt.serialize());
284289
setInnerHTML({ element: blockRef.current, block });
285290
}
286291
}, [getTextAndStylesHash(block)]);
@@ -296,7 +301,20 @@ export const Block: React.FC<BlockProps> = memo(
296301
return (
297302
// TODO: eslint 규칙을 수정해야 할까?
298303
// TODO: ol일때 index 순서 처리
299-
<div data-testid={testKey} style={{ position: "relative" }}>
304+
<div
305+
data-testid={testKey}
306+
data-id={id}
307+
data-index={virtualIndex}
308+
key={virtualIndex}
309+
ref={virtualRef}
310+
style={{
311+
position: "absolute",
312+
top: 0,
313+
left: 0,
314+
width: "100%",
315+
transform: `translateY(${virtualStart}px)`,
316+
}}
317+
>
300318
{showTopIndicator && <Indicator />}
301319
<motion.div
302320
ref={setNodeRef}

client/src/features/editor/hooks/useMarkdownGrammer.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ export const useMarkdownGrammer = ({
270270
editorCRDT.currentBlock = targetBlock;
271271
setCaretPosition({
272272
blockId: targetBlock.id,
273-
linkedList: editorCRDT.LinkedList,
273+
274274
position: targetBlock.crdt.read().length,
275275
pageId,
276276
});
@@ -437,7 +437,7 @@ export const useMarkdownGrammer = ({
437437
currentBlock.crdt.currentCaret = e.key === "Home" ? 0 : currentBlock.crdt.read().length;
438438
setCaretPosition({
439439
blockId: currentBlock.id,
440-
linkedList: editorCRDT.LinkedList,
440+
441441
position: currentBlock.crdt.currentCaret,
442442
pageId,
443443
});
@@ -456,7 +456,7 @@ export const useMarkdownGrammer = ({
456456
editorCRDT.currentBlock = headBlock;
457457
setCaretPosition({
458458
blockId: headBlock.id,
459-
linkedList: editorCRDT.LinkedList,
459+
460460
position: currentCaretPosition,
461461
pageId,
462462
});
@@ -478,7 +478,7 @@ export const useMarkdownGrammer = ({
478478
editorCRDT.currentBlock = lastBlock;
479479
setCaretPosition({
480480
blockId: lastBlock.id,
481-
linkedList: editorCRDT.LinkedList,
481+
482482
position: currentCaretPosition,
483483
pageId,
484484
});
@@ -516,7 +516,7 @@ export const useMarkdownGrammer = ({
516516
editorCRDT.currentBlock = targetBlock;
517517
setCaretPosition({
518518
blockId: targetBlock.id,
519-
linkedList: editorCRDT.LinkedList,
519+
520520
position: Math.min(caretPosition, targetBlock.crdt.read().length),
521521
pageId,
522522
});
@@ -547,7 +547,7 @@ export const useMarkdownGrammer = ({
547547
editorCRDT.currentBlock = targetBlock;
548548
setCaretPosition({
549549
blockId: targetBlock.id,
550-
linkedList: editorCRDT.LinkedList,
550+
551551
position: targetBlock.crdt.read().length,
552552
pageId,
553553
});
@@ -573,7 +573,7 @@ export const useMarkdownGrammer = ({
573573
editorCRDT.currentBlock = targetBlock;
574574
setCaretPosition({
575575
blockId: targetBlock.id,
576-
linkedList: editorCRDT.LinkedList,
576+
577577
position: 0,
578578
pageId,
579579
});

client/src/utils/caretUtils.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { BlockLinkedList, TextLinkedList } from "@noctaCrdt/LinkedList";
21
import { BlockId } from "@noctaCrdt/NodeId";
32

43
interface SetCaretPositionProps {
54
blockId: BlockId;
6-
linkedList: BlockLinkedList | TextLinkedList;
75
clientX?: number;
86
clientY?: number;
97
position?: number; // Used to set the caret at a specific position
@@ -72,28 +70,20 @@ export const getAbsoluteCaretPosition = (element: HTMLElement): number => {
7270
return 0;
7371
};
7472

75-
export const setCaretPosition = ({
76-
blockId,
77-
linkedList,
78-
position,
79-
pageId,
80-
}: SetCaretPositionProps): void => {
73+
export const setCaretPosition = ({ blockId, position, pageId }: SetCaretPositionProps): void => {
8174
try {
8275
if (position === undefined) return;
8376
const selection = window.getSelection();
8477
if (!selection) return;
8578

8679
const currentPage = document.getElementById(pageId);
8780

88-
const blockElements = Array.from(
89-
currentPage?.querySelectorAll('.d_flex.pos_relative.w_full[data-group="true"]') || [],
81+
const targetElement = currentPage?.querySelector(
82+
`[data-id="${blockId.serialize().client}-${blockId.serialize().clock}"]`,
9083
);
91-
92-
const currentIndex = linkedList.spread().findIndex((b) => b.id === blockId);
93-
const targetElement = blockElements[currentIndex];
9484
if (!targetElement) return;
9585

96-
const editableDiv = targetElement.querySelector('[contenteditable="true"]') as HTMLDivElement;
86+
const editableDiv = targetElement.querySelector('[contenteditable="true"]') as HTMLElement;
9787
if (!editableDiv) return;
9888

9989
editableDiv.focus();

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)