Intersection Observer API 활용하여 React에서 무한스크롤 구현하기
✔️ Infinite Scroll?
무한스크롤(Infinite Scroll) 이란 말 그대로 스크롤을 무한으로 할 수 있는 기능을 말합니다. 불러와야 하는 게시글 목록이 많은 경우 무한스크롤을 활용하면 사용자 편의성과 클라이언트 부담을 덜 수 있다는 장점이 있습니다.
✔️ Intersection Observer API 란?
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 API입니다.
MDN에서는 Intersection Observer API를 위와 같이 정의하고 있습니다. 쉽게 말하면 관찰 중인 요소가 사용자가 보고 있는 영역 안에 들어왔는지 알려주는 API라고 할 수 있습니다.
✔️ Options
1
const io = new IntersectionObserver(callback, options);
옵션부에는 root, rootMargin, threshold 3가지가 있습니다.
root
: 타겟 엘리먼트가 보이는지 안보이는지 결정할 뷰포트로 사용되는 엘리먼트 (기본값은 Browser Viewport)rootMargin
: root에 정의된 Element가 가진 마진값 (기본값은 0)threshold
: 타겟 엘리먼트가 root에 정의된 Element에 얼마나 노출되었을 때 Callback 함수를 실행할지 정의하는 옵션 (기본값은 0)
1
2
const io = new IntersectionObserver(callback, { threshold: 0.7 });
io.observe(관찰할 요소);
예를 들어 관찰하고자 하는 대상이 화면 내 70% 안에 들어왔을 경우 위와 같이 작성할 수 있습니다.
✔️ 콜백함수
1
2
3
4
5
6
7
8
9
10
11
const callback = (entries, io) => {
entries.forEach((entry) => {
// 화면 안에 요소가 들어왔는지 체크
if (entry.isIntersecting) {
// 기존 관찰하던 요소는 더 이상 관찰하지 않음
io.unobserve(entry.target);
// TODO: 새로운 컨텐츠 추가
// TODO: 새로운 컨텐츠의 마지막 요소를 관찰 시작
}
});
};
콜백 함수는 위와 같이 entries와 io를 매개변수로 갖습니다.
Intersection Observer를 쓰기 이전에는 무한스크롤을 구현할 때 성능 문제로 인해 debounce
나 throttle
과 같은 호출수 제한 최적화 작업이 반드시 이뤄져야 했지만 지금은 필요하지 않습니다.
✔️ 무한스크롤 구현
이제 본격적으로 무한스크롤 구현 코드를 작성해보도록 하겠습니다.
1
2
$ npx create-react-app infinite-scroll-example
$ npm i styled-components react-loading
우선 위와 같은 CRA 명령어를 입력하여 초기 세팅을 하고 styled와 react-loading 라이브러리를 설치해주도록 하겠습니다.
- Item.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react";
import styled from "styled-components";
const ItemWrap = styled.div`
width: 350px;
height: 370px;
padding-top: 140px;
font-size: 70px;
display: flex;
flex-direction: column;
background-color: #ffffff;
margin: 1rem;
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
border-radius: 6px;
`;
const Item = ({ number }) => {
return <ItemWrap>{number}</ItemWrap>;
};
export default Item;
- Loader.js => 로딩 스피너 컴포넌트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import ReactLoading from "react-loading";
import styled from "styled-components";
const LoaderWrap = styled.div`
width: 100%;
height: 80%;
display: flex;
justify-content: center;
text-align: center;
align-items: center;
`;
const Loader = () => {
return (
<LoaderWrap>
<ReactLoading type="spin" color="#455560" />
</LoaderWrap>
);
};
export default Loader;
- App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import React, { useEffect, useState } from "react";
import styled, { createGlobalStyle } from "styled-components";
import Item from "./Item";
import Loader from "./Loader";
const GlobalStyle = createGlobalStyle`
*, *::before, *::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
background-color: #f2f5f7;
}
`;
const AppWrap = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-items: center;
.Target-Element {
width: 100vw;
height: 140px;
display: flex;
justify-content: center;
text-align: center;
align-items: center;
}
`;
const App = () => {
const [target, setTarget] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [itemLists, setItemLists] = useState([1]);
useEffect(() => {
console.log(itemLists);
}, [itemLists]);
const getMoreItem = async () => {
setIsLoaded(true);
await new Promise((resolve) => setTimeout(resolve, 1500));
let Items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
setItemLists((itemLists) => itemLists.concat(Items));
setIsLoaded(false);
};
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoaded) {
observer.unobserve(entry.target);
await getMoreItem();
observer.observe(entry.target);
}
};
useEffect(() => {
let observer;
if (target) {
observer = new IntersectionObserver(onIntersect, {
threshold: 0.4,
});
observer.observe(target);
}
return () => observer && observer.disconnect();
}, [target]);
return (
<>
<GlobalStyle />
<AppWrap>
{itemLists.map((v, i) => {
return <Item number={i + 1} key={i} />;
})}
<div ref={setTarget} className="Target-Element">
{isLoaded && <Loader />}
</div>
</AppWrap>
</>
);
};
export default App;
✔️ 적용 화면
✔️ 참고 사이트
- https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API
- https://velog.io/@yejinh/Intersection-Observer%EB%A1%9C-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
- https://velog.io/@jce1407/React-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-Intersection-Observer
🔔포스팅 공지
개인 공부 기록용 블로그 입니다.
잘못된 부분이 있을 시 메일이나 댓글로 지적해주시면 감사드리겠습니다 :)
댓글남기기