Skip to content

Commit 2fb1333

Browse files
authored
Merge pull request #930 from chysis/refactor/861-detail-render-performance
refactor: Detail 렌더링 최적화
2 parents 3583f7b + adf56db commit 2fb1333

4 files changed

Lines changed: 196 additions & 49 deletions

File tree

packages/view/src/components/Detail/Detail.hook.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { useState } from "react";
1+
import { useState, useCallback, useMemo } from "react";
2+
import { CellMeasurerCache } from "react-virtualized";
3+
import type { RenderedRows } from "react-virtualized/dist/es/List";
24

35
import type { CommitNode } from "types";
46

7+
import type { VirtualizedItem } from "./Detail.type";
58
import { getSummaryCommitList } from "./Detail.util";
69

710
type UseToggleHook = [boolean, () => void];
@@ -23,3 +26,50 @@ export const useCommitListHide = (commitNodeListInCluster: CommitNode[]) => {
2326
commitNodeList,
2427
};
2528
};
29+
30+
export const useVirtualizedList = (commitNodeListInCluster: CommitNode[]) => {
31+
const [showScrollIndicator, setShowScrollIndicator] = useState(false);
32+
33+
const cache = useMemo(
34+
() =>
35+
new CellMeasurerCache({
36+
fixedWidth: true,
37+
defaultHeight: 120,
38+
}),
39+
[]
40+
);
41+
42+
const virtualizedItems = useMemo((): VirtualizedItem[] => {
43+
const items: VirtualizedItem[] = [];
44+
45+
items.push({
46+
type: "summary",
47+
data: commitNodeListInCluster,
48+
});
49+
50+
commitNodeListInCluster.forEach(({ commit }) => {
51+
items.push({
52+
type: "commit",
53+
data: commit,
54+
});
55+
});
56+
57+
return items;
58+
}, [commitNodeListInCluster]);
59+
60+
// scrollable indicator handler
61+
const handleRowsRendered = useCallback(
62+
({ stopIndex }: RenderedRows) => {
63+
const lastIndex = virtualizedItems.length - 1;
64+
setShowScrollIndicator(stopIndex < lastIndex);
65+
},
66+
[virtualizedItems.length]
67+
);
68+
69+
return {
70+
cache,
71+
virtualizedItems,
72+
showScrollIndicator,
73+
handleRowsRendered,
74+
};
75+
};

packages/view/src/components/Detail/Detail.scss

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
justify-content: space-between;
66
align-items: center;
77
font-size: $font-size-caption;
8+
padding-bottom: 1rem;
89

910
.detail__summary-divider {
1011
margin-right: 1rem;
@@ -45,13 +46,14 @@
4546
}
4647
}
4748

48-
.detail__commit-list {
49+
.detail__container {
50+
position: relative;
4951
width: 100%;
50-
display: flex;
51-
flex-direction: column;
52-
padding: 1.25rem;
53-
font-size: $font-size-body;
52+
height: 100%;
53+
min-height: 200px;
54+
}
5455

56+
.detail__container {
5557
.detail__commit-item {
5658
width: 100%;
5759
display: flex;
@@ -181,15 +183,65 @@
181183
}
182184
}
183185

184-
.detail__toggle-button {
186+
.detail__scroll-indicator {
187+
position: absolute;
188+
bottom: 1rem;
189+
left: 50%;
190+
transform: translateX(-50%);
191+
z-index: 1000;
185192
display: flex;
186-
margin: 0 auto;
187-
border: none;
188-
background: none;
189-
color: var(--color-tertiary);
193+
align-items: center;
194+
justify-content: center;
195+
width: 2rem;
196+
height: 2rem;
197+
background: rgba(0, 0, 0, 0.1);
198+
border-radius: 50%;
199+
color: var(--color-text-tertiary);
190200
cursor: pointer;
201+
transition: all 0.3s ease;
202+
backdrop-filter: blur(4px);
203+
border: 1px solid rgba(255, 255, 255, 0.1);
191204

192205
&:hover {
193-
color: $color-light-gray;
206+
background: rgba(0, 0, 0, 0.2);
207+
color: var(--color-text-secondary);
208+
transform: translateX(-50%) translateY(-2px);
209+
}
210+
211+
svg {
212+
font-size: 1rem;
213+
animation: bounce 2s infinite;
214+
}
215+
216+
@keyframes bounce {
217+
0%, 20%, 50%, 80%, 100% {
218+
transform: translateY(0);
219+
}
220+
40% {
221+
transform: translateY(-3px);
222+
}
223+
60% {
224+
transform: translateY(-2px);
225+
}
226+
}
227+
}
228+
229+
.commit-message__issue-link {
230+
color: var(--color-primary);
231+
text-decoration: none;
232+
font-weight: 500;
233+
padding: 0.125rem 0.25rem;
234+
border-radius: 0.125rem;
235+
background-color: rgba(224, 96, 145, 0.1);
236+
transition: all 0.2s ease-in-out;
237+
238+
&:hover {
239+
background-color: rgba(224, 96, 145, 0.2);
240+
text-decoration: underline;
241+
transform: translateY(-1px);
242+
}
243+
244+
&:active {
245+
transform: translateY(0);
194246
}
195247
}

packages/view/src/components/Detail/Detail.tsx

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from "react";
1+
import React, { useCallback, useMemo } from "react";
22
import dayjs from "dayjs";
33
import {
44
AddCircleRounded,
@@ -7,18 +7,18 @@ import {
77
CommitRounded,
88
RestorePageRounded,
99
ExpandMoreRounded,
10-
ExpandLessRounded,
1110
} from "@mui/icons-material";
1211
import { Tooltip } from "@mui/material";
12+
import type { ListRowProps } from "react-virtualized";
13+
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
1314

1415
import { useGithubInfo, useDataStore } from "store";
1516
import { Author } from "components/Common/Author";
1617
import type { IssueLinkedMessage } from "components/Common/GithubIssueLink";
1718
import { renderIssueLinkedNodes } from "components/Common/GithubIssueLink";
1819

19-
import { useCommitListHide } from "./Detail.hook";
2020
import { getCommitListDetail } from "./Detail.util";
21-
import { FIRST_SHOW_NUM } from "./Detail.const";
21+
import { useVirtualizedList } from "./Detail.hook";
2222
import type { DetailProps, DetailSummaryProps, DetailSummaryItem, CommitItemProps } from "./Detail.type";
2323

2424
import "./Detail.scss";
@@ -32,14 +32,52 @@ const Detail = ({ clusterId, authSrcMap }: DetailProps) => {
3232
selectedData?.filter((selected) => selected.commitNodeList[0].clusterId === clusterId)[0].commitNodeList ?? [],
3333
[selectedData, clusterId]
3434
);
35-
const { commitNodeList, toggle, handleToggle } = useCommitListHide(commitNodeListInCluster);
36-
37-
const isShow = commitNodeListInCluster.length > FIRST_SHOW_NUM;
3835

3936
const handleCommitIdCopy = (id: string) => async () => {
4037
navigator.clipboard.writeText(id);
4138
};
4239

40+
const { cache, virtualizedItems, showScrollIndicator, handleRowsRendered } =
41+
useVirtualizedList(commitNodeListInCluster);
42+
43+
const renderCommitItem = useCallback(
44+
(props: { index: number; key: string }) => {
45+
const { index, key } = props;
46+
const item = virtualizedItems[index];
47+
48+
if (item.type === "summary") {
49+
return <DetailSummary commitNodeListInCluster={item.data} />;
50+
}
51+
return (
52+
<MemoizedCommitItem
53+
key={key}
54+
commit={item.data}
55+
owner={owner}
56+
repo={repo}
57+
authSrcMap={authSrcMap}
58+
handleCommitIdCopy={handleCommitIdCopy}
59+
linkedMessage={issueLinkedMessage}
60+
/>
61+
);
62+
},
63+
[virtualizedItems]
64+
);
65+
66+
const rowRenderer = useCallback(
67+
({ index, key, parent, style }: ListRowProps) => (
68+
<CellMeasurer
69+
key={key}
70+
cache={cache}
71+
parent={parent}
72+
columnIndex={0}
73+
rowIndex={index}
74+
>
75+
<div style={style}>{renderCommitItem({ index, key })}</div>
76+
</CellMeasurer>
77+
),
78+
[cache, renderCommitItem]
79+
);
80+
4381
const issueLinkedMessage: IssueLinkedMessage = useMemo(() => {
4482
const message = commitNodeListInCluster?.[0]?.commit?.message;
4583
if (!message) return { title: [], body: null };
@@ -56,32 +94,30 @@ const Detail = ({ clusterId, authSrcMap }: DetailProps) => {
5694
if (!selectedData || selectedData.length === 0) return null;
5795

5896
return (
59-
<>
60-
<DetailSummary commitNodeListInCluster={commitNodeListInCluster} />
61-
<ul className="detail__commit-list">
62-
{commitNodeList.map(({ commit }) => (
63-
<CommitItem
64-
key={commit.id}
65-
commit={commit}
66-
owner={owner}
67-
repo={repo}
68-
authSrcMap={authSrcMap}
69-
handleCommitIdCopy={handleCommitIdCopy}
70-
linkedMessage={issueLinkedMessage}
71-
/>
72-
))}
73-
</ul>
74-
75-
{isShow && (
76-
<button
77-
type="button"
78-
className="detail__toggle-button"
79-
onClick={handleToggle}
80-
>
81-
{toggle ? <ExpandLessRounded /> : <ExpandMoreRounded />}
82-
</button>
97+
<div className="detail__container">
98+
<AutoSizer>
99+
{({ height, width }) => {
100+
return (
101+
<List
102+
height={height}
103+
width={width}
104+
rowCount={virtualizedItems.length}
105+
rowHeight={cache.rowHeight}
106+
rowRenderer={rowRenderer}
107+
onRowsRendered={handleRowsRendered}
108+
className="detail__virtualized-list"
109+
estimatedRowSize={120}
110+
/>
111+
);
112+
}}
113+
</AutoSizer>
114+
115+
{showScrollIndicator && (
116+
<div className="detail__scroll-indicator">
117+
<ExpandMoreRounded />
118+
</div>
83119
)}
84-
</>
120+
</div>
85121
);
86122
};
87123

@@ -90,10 +126,7 @@ export default Detail;
90126
function CommitItem({ commit, owner, repo, authSrcMap, handleCommitIdCopy, linkedMessage }: CommitItemProps) {
91127
const { id, message, author, commitDate } = commit;
92128
return (
93-
<li
94-
key={id}
95-
className="detail__commit-item"
96-
>
129+
<div className="detail__commit-item">
97130
<div className="commit-item__detail">
98131
<div className="commit-item__message-container">
99132
<div className="commit-message">
@@ -148,10 +181,12 @@ function CommitItem({ commit, owner, repo, authSrcMap, handleCommitIdCopy, linke
148181
</div>
149182
</div>
150183
</div>
151-
</li>
184+
</div>
152185
);
153186
}
154187

188+
const MemoizedCommitItem = React.memo(CommitItem);
189+
155190
function DetailSummary({ commitNodeListInCluster }: DetailSummaryProps) {
156191
const { authorLength, fileLength, commitLength, insertions, deletions } = getCommitListDetail({
157192
commitNodeListInCluster,

packages/view/src/components/Detail/Detail.type.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,13 @@ export interface CommitItemProps {
2727
handleCommitIdCopy: (id: string) => () => Promise<void>;
2828
linkedMessage: IssueLinkedMessage;
2929
}
30+
31+
export type VirtualizedItem =
32+
| {
33+
type: "summary";
34+
data: ClusterNode["commitNodeList"];
35+
}
36+
| {
37+
type: "commit";
38+
data: Commit;
39+
};

0 commit comments

Comments
 (0)