Skip to content

Commit 54949f2

Browse files
committed
feat: 반복요소 컴포넌트화
1 parent 95c3cf9 commit 54949f2

File tree

15 files changed

+179
-83
lines changed

15 files changed

+179
-83
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@
55
화면에 실제로 보여지는 부분만 DOM으로 렌더링하도록 연산하여 시스템 부하와 과도하게 낭비되는 리소스를 방지 하여 더 나은 서비스를 제공할 수 있습니다.
66

77
## 일반적인 목록 vs 가상화된 목록 비교
8+
89
일반적으로 목록을 많이 보여주는 화면과 react-virtualized를 사용하여 최적화 한 차이를 비교 제공합니다.
10+
11+
## 공식문서
12+
13+
- https://github.com/bvaughn/react-virtualized

src/App.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
2-
import { ChakraProvider, Flex, Container } from '@chakra-ui/react';
2+
import { ChakraProvider, Flex, Container, Heading } from '@chakra-ui/react';
33

44
import Navigation from './components/Navigation';
55
import TextList from './pages/TextList';
@@ -13,7 +13,12 @@ const App = () => {
1313
<Flex>
1414
<Container width="700px" padding={`20px 15px`}>
1515
<Router>
16-
<Navigation />
16+
<Container padding={`0 0 20px 0`} marginBottom={5} borderBottom={'solid 1px #bbb'}>
17+
<Heading mb={5} textAlign="center">
18+
React virtualized examples
19+
</Heading>
20+
<Navigation />
21+
</Container>
1722
<Switch>
1823
<Route exact path="/" component={TextList} />
1924
<Route path="/text-list" component={TextList} />
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import './index.scss';
2+
3+
interface Props {
4+
index: number;
5+
imageUrl: string;
6+
title: string;
7+
onLoad?: () => void;
8+
}
9+
10+
const ImageListItem = ({ index, imageUrl, title, onLoad }: Props) => {
11+
return (
12+
<div className="image-list-item">
13+
<section className="thumb-wrap">
14+
<img src={imageUrl} alt="" onLoad={() => onLoad?.()} />
15+
</section>
16+
<section>
17+
<p>index: {index}</p>
18+
<p>
19+
{title} {title} {title} {title} {title} {title}
20+
</p>
21+
</section>
22+
</div>
23+
);
24+
};
25+
26+
export default ImageListItem;

src/components/Navigation/index.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.navigation {
2+
a {
3+
padding: 10px;
4+
background-color: #bbb;
5+
text-align: center;
6+
7+
&.selected {
8+
background-color: #61dafb;
9+
font-weight: bold;
10+
}
11+
}
12+
}

src/components/Navigation/index.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
import { NavLink } from 'react-router-dom';
2-
import { Container, Heading, SimpleGrid } from '@chakra-ui/react';
2+
import { SimpleGrid } from '@chakra-ui/react';
3+
import './index.scss';
34

45
const Naviagation = () => {
56
return (
6-
<Container padding={`0 0 20px 0`} marginBottom={5} borderBottom={'solid 1px #bbb'}>
7-
<Heading mb={5} textAlign="center">
8-
React virtualized examples
9-
</Heading>
10-
<SimpleGrid className="navigation" columns={2} spacingX="5px" spacingY="20px">
11-
<NavLink to="/text-list" activeClassName="selected">
12-
텍스트 목록
13-
</NavLink>
14-
<NavLink to="/text-list-virtualized" activeClassName="selected">
15-
텍스트 목록 (with virtualized)
16-
</NavLink>
17-
<NavLink to="/image-list" activeClassName="selected">
18-
이미지 목록
19-
</NavLink>
20-
<NavLink to="/image-list-virtualized" activeClassName="selected">
21-
이미지 목록 (with virtualized)
22-
</NavLink>
23-
</SimpleGrid>
24-
</Container>
7+
<SimpleGrid className="navigation" columns={2} spacingX="5px" spacingY="20px">
8+
<NavLink to="/text-list" activeClassName="selected">
9+
텍스트 목록
10+
</NavLink>
11+
<NavLink to="/text-list-virtualized" activeClassName="selected">
12+
텍스트 목록 (with virtualized)
13+
</NavLink>
14+
<NavLink to="/image-list" activeClassName="selected">
15+
이미지 목록
16+
</NavLink>
17+
<NavLink to="/image-list-virtualized" activeClassName="selected">
18+
이미지 목록 (with virtualized)
19+
</NavLink>
20+
</SimpleGrid>
2521
);
2622
};
2723

src/components/TextListItem/index.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import './index.scss';
2+
3+
interface Props {
4+
index: number;
5+
email: string;
6+
name: string;
7+
body: string;
8+
}
9+
10+
const TextListItem = ({ index, email, name, body }: Props) => {
11+
return (
12+
<div className="text-list-item">
13+
<p>index: {index}</p>
14+
<p>email: {email}</p>
15+
<p>name: {name}</p>
16+
<p>
17+
body: {body} {body}
18+
</p>
19+
</div>
20+
);
21+
};
22+
23+
export default TextListItem;

src/pages/ImageList/index.scss

Whitespace-only changes.

src/pages/ImageList/index.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,63 @@
11
import { useEffect, useState, useCallback } from 'react';
2+
import { checkInfiniteScrollPosition } from '../../helpers/scroll';
3+
import { throttle } from 'lodash-es';
4+
25
import { Container, Heading, Button, Text } from '@chakra-ui/react';
36
import StackSkleton from '../../components/StackSkeleton';
4-
import './index.scss';
7+
import ImageListItem from '../../components/ImageListItem';
58

6-
export interface ImageListItem {
9+
export interface ImageListItemState {
710
id: number;
811
title: string;
12+
url: string;
913
thumbnailUrl: string;
1014
}
1115

16+
export const SPLICE_SIZE = 500;
17+
18+
let totalList: ImageListItemState[] = [];
19+
1220
const ImageList = () => {
13-
const [list, setList] = useState<ImageListItem[]>([]);
21+
const [list, setList] = useState<ImageListItemState[]>([]);
1422

15-
const addList = useCallback(() => {
23+
const fetchData = useCallback(async () => {
24+
// const res = await fetch('https://jsonplaceholder.typicode.com/photos');
25+
// console.log('res :>> ', res);
1626
fetch('https://jsonplaceholder.typicode.com/photos').then((res) => {
1727
const data = res.json();
1828

1929
data.then((newList) => {
20-
setList([...list, ...newList]);
30+
totalList = newList;
31+
addList();
2132
});
2233
});
23-
}, [list, setList]);
34+
}, []);
35+
36+
const addList = useCallback(() => {
37+
if (!totalList.length) {
38+
return;
39+
}
40+
41+
const data = totalList.splice(0, SPLICE_SIZE);
42+
setList([...list, ...data]);
43+
}, [list]);
44+
45+
const onScroll = useCallback(() => {
46+
const isNeedFetching = checkInfiniteScrollPosition({ bottom: 600 });
47+
if (isNeedFetching) {
48+
addList();
49+
}
50+
}, [addList]);
2451

2552
useEffect(() => {
26-
addList();
27-
}, []);
53+
fetchData();
54+
}, [fetchData]);
55+
56+
useEffect(() => {
57+
const onScrollTrottle = throttle(onScroll, 100);
58+
window.addEventListener('scroll', onScrollTrottle);
59+
return () => window.removeEventListener('scroll', onScrollTrottle);
60+
}, [onScroll]);
2861

2962
return (
3063
<>
@@ -43,11 +76,7 @@ const ImageList = () => {
4376
<section>
4477
{list.length ? (
4578
list.map(({ title, thumbnailUrl }, index) => (
46-
<div className="text-list-item">
47-
<div>index: {index}</div>
48-
<div>title: {title}</div>
49-
<div>body: {thumbnailUrl}</div>
50-
</div>
79+
<ImageListItem index={index} imageUrl={thumbnailUrl} title={title} />
5180
))
5281
) : (
5382
<StackSkleton count={5} />

src/pages/ImageListVirtualized/index.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,33 @@ import { WindowScroller, CellMeasurer, CellMeasurerCache, AutoSizer, List, ListR
33
import { checkInfiniteScrollPosition } from '../../helpers/scroll';
44
import { throttle } from 'lodash-es';
55

6-
import { ImageListItem } from '../ImageList';
6+
import { ImageListItemState, SPLICE_SIZE } from '../ImageList';
77
import { Container, Heading, Button, Text } from '@chakra-ui/react';
8+
import ImageListItem from '../../components/ImageListItem';
89
import StackSkleton from '../../components/StackSkeleton';
910

10-
import './index.scss';
11-
1211
const cellCache = new CellMeasurerCache({
1312
defaultWidth: 100,
1413
fixedWidth: true,
1514
});
1615

17-
let totalList: ImageListItem[] = [];
16+
let totalList: ImageListItemState[] = [];
1817

1918
const ImageListVirtualized = () => {
20-
const [list, setList] = useState<ImageListItem[]>([]);
19+
const [list, setList] = useState<ImageListItemState[]>([]);
2120
const listRef = useRef<List>(null);
2221

2322
const rowRenderer = ({ index, key, parent, style }: ListRowProps) => {
2423
return (
2524
<CellMeasurer cache={cellCache} parent={parent} key={key} columnIndex={0} rowIndex={index}>
2625
{({ measure }) => (
2726
<div style={style} key={index}>
28-
<div className="image-list-item">
29-
<section className="thumb-wrap">
30-
<img src={list[index].thumbnailUrl} alt="" onLoad={measure} />
31-
</section>
32-
<section>
33-
<p>index: {index}</p>
34-
<p>title: {list[index].title}</p>
35-
</section>
36-
</div>
27+
<ImageListItem
28+
index={index}
29+
imageUrl={list[index].thumbnailUrl}
30+
title={list[index].title}
31+
onLoad={measure} // 중요: measure 함수로 이미지가 로드된 이후 재 측정을 해주어야 정확한 사이즈로 랜더링됩니다.
32+
/>
3733
</div>
3834
)}
3935
</CellMeasurer>
@@ -58,7 +54,7 @@ const ImageListVirtualized = () => {
5854
return;
5955
}
6056

61-
const data = totalList.splice(0, 100);
57+
const data = totalList.splice(0, SPLICE_SIZE);
6258
setList([...list, ...data]);
6359
}, [list]);
6460

@@ -104,8 +100,8 @@ const ImageListVirtualized = () => {
104100
autoHeight
105101
height={height}
106102
width={width}
107-
isScrolling={isScrolling}
108103
overscanRowCount={0}
104+
isScrolling={isScrolling}
109105
onScroll={onChildScroll}
110106
scrollTop={scrollTop}
111107
rowCount={list.length}

src/pages/TextList/index.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
import { useEffect, useState, useCallback } from 'react';
1+
import { useEffect, useState, useCallback, Fragment } from 'react';
2+
import { checkInfiniteScrollPosition } from '../../helpers/scroll';
3+
import { throttle } from 'lodash-es';
4+
25
import { Container, Heading, Button, Text } from '@chakra-ui/react';
36
import StackSkleton from '../../components/StackSkeleton';
4-
import './index.scss';
7+
import TextListItem from '../../components/TextListItem';
58

6-
export interface TextListItem {
9+
export interface TextListItemState {
710
id: number;
811
name: string;
912
email: string;
1013
body: string;
1114
}
1215

16+
let isFetching: boolean = false;
17+
1318
const TextList = () => {
14-
const [list, setList] = useState<TextListItem[]>([]);
19+
const [list, setList] = useState<TextListItemState[]>([]);
1520

1621
const addList = useCallback(() => {
22+
isFetching = true;
1723
fetch('https://jsonplaceholder.typicode.com/comments').then((res) => {
1824
const data = res.json();
1925

2026
data.then((newList) => {
2127
setList([...list, ...newList]);
28+
isFetching = false;
2229
});
2330
});
2431
}, [list, setList]);
@@ -27,6 +34,23 @@ const TextList = () => {
2734
addList();
2835
}, []);
2936

37+
const onScroll = useCallback(() => {
38+
if (isFetching) {
39+
return false;
40+
}
41+
42+
const isNeedFetching = checkInfiniteScrollPosition({ bottom: 600 });
43+
if (isNeedFetching) {
44+
addList();
45+
}
46+
}, [addList]);
47+
48+
useEffect(() => {
49+
const onScrollTrottle = throttle(onScroll, 100);
50+
window.addEventListener('scroll', onScrollTrottle, { passive: true });
51+
return () => window.removeEventListener('scroll', onScrollTrottle);
52+
}, [onScroll]);
53+
3054
return (
3155
<>
3256
<Container padding={0} marginBottom={5} textAlign="center">
@@ -44,12 +68,9 @@ const TextList = () => {
4468
<section>
4569
{list.length ? (
4670
list.map(({ name, email, body }, index) => (
47-
<div className="text-list-item" key={index}>
48-
<p>index: {index}</p>
49-
<p>email: {email}</p>
50-
<p>name: {name}</p>
51-
<p>body: {body}</p>
52-
</div>
71+
<Fragment key={index}>
72+
<TextListItem index={index} email={email} name={name} body={body} />
73+
</Fragment>
5374
))
5475
) : (
5576
<StackSkleton count={5} />

src/pages/TextListVirtualized/index.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)