React에서 IntersectionObserver API로 infiniteScroll 구현하기

시작하기

IntersectionObserver API 란?

기존에는 infiniteScroll을 구현하려면 Element.getBoundingClientRect(), addEventListener에 scroll, resize등을 사용해야만 했습니다.

IntersectionObserver API는 대상 요소와 상위 요소 또는 최상위 문서의 뷰포트 와의 교차에서 변경 사항을 비동기 적으로 관찰하는 방법을 제공합니다.

사용법

1
2
3
4
5
6
7
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};

let observer = new IntersectionObserver(callback, options);

callback

뷰포트와 타겟이 겹쳤을때 실행되는 함수 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let callback = (entries, observer) => {
entries.forEach((entry) => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};

options

뷰포트와 그 범위를 설정해줄수 있습니다.

  • root
    대상의 가시성을 확인하기위한 뷰포트로 사용되는 요소입니다. 대상의 조상이어야합니다. 지정하지 않거나 if 인 경우 기본적으로 브라우저 뷰포트로 설정됩니다 null.

  • rootMargin
    뿌리 주위의 여백. CSS margin속성 과 유사한 값을 가질 수 있습니다 10px 20px 30px 40px”( 예 : “ (상단, 우측, 하단, 좌측). 값은 백분율 일 수 있습니다.이 값 세트는 교차점을 계산하기 전에 루트 요소의 경계 상자의 양쪽을 늘리거나 줄 이도록합니다. 모두 0입니다.

  • threshold
    관찰자의 콜백을 실행해야하는 대상 가시성 비율을 나타내는 단일 숫자 또는 숫자 배열입니다. 가시성이 50 %를 초과하는 시점 만 감지하려는 경우 0.5 값을 사용할 수 있습니다. 가시성이 다른 25 %를 초과 할 때마다 콜백을 실행하려면 배열 [0, 0.25, 0.5, 0.75, 1]을 지정합니다. 기본값은 0입니다 (하나의 픽셀 만 표시되면 콜백이 실행됨을 의미). 1.0 값은 모든 픽셀이 표시 될 때까지 임계 값이 전달 된 것으로 간주되지 않음을 의미합니다.

React에서 구현하기

기본 jsx, css를 작성해줍니다.

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
import React, { useState } from "react";

const App = () => {
const [datas, setData] = useState([
{ name: "기린", id: 0 },
{ name: "강아지", id: 1 },
{ name: "토끼", id: 2 },
{ name: "호랑이", id: 3 },
{ name: "사자", id: 4 },
]);

return (
<div className="wrapper">
<section className="card-grid" id="target-root">
{datas.map((animal, index) => {
return (
<div key={index} className="card">
<p>아이디: {animal.id}</p>
<p>이름:{animal.name}</p>
</div>
);
})}
</section>
</div>
);
};

export default App;
App.css
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
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}

.card-grid {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 350px;
border: 1px solid black;
overflow: auto;
}

.card {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border: 2px solid black;
width: 50%;
padding: 40px 20px;
margin: 20px;
font-weight: bold;
}

.last {
background-color: purple;
color: white;
}

p {
margin: 5px;
}

css를 추가합니다.

App.js
1
2
3
//...
import "./App.css";
//...

이제 본격적으로 구현해보도록 하겠습니다.
인피니티 스크롤은 보이는 부분이 마지막 카드에 도달했을때 새롭게 불러오는 방식입니다.
ref를 두가지로 정해줍니다. 1. 뷰포트, 2. 교차할 마지막 카드

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
import React, { useRef, useState } from "react";

const App = () => {
const [datas, setData] = useState([
{ name: "기린", id: 0 },
{ name: "강아지", id: 1 },
{ name: "토끼", id: 2 },
{ name: "호랑이", id: 3 },
{ name: "사자", id: 4 },
]);
const viewport = useRef(null);
const target = useRef(null);

return (
<div className="wrapper">
<section className="card-grid" id="target-root" ref={viewport}>
{datas.map((animal, index) => {
const lastEl = index === datas.length - 1;
return (
<div
key={index}
className={`card ${lastEl && "last"}`}
ref={lastEl ? target : null}
>
<p>아이디: {animal.id}</p>
<p>이름:{animal.name}</p>
</div>
);
})}
</section>
</div>
);
};

export default App;

기본 구조는 완료되었습니다.
이제 intersection observer api를 사용해보도록 하겠습니다.

App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//...
const loadItems = () => {
setData((prevState) => {
const animals = [
{ name: "고양이" },
{ name: "코끼리" },
{ name: "원숭이" },
{ name: "고라니" },
{ name: "기린" },
{ name: "표범" },
];
const id = prevState[prevState.length - 1].id;
const animalId = animals.map((animal, index) => {
return { ...animal, id: id + index + 1 };
});
return [...prevState, ...animalId];
});
};
//...

마지막 카드와 뷰포트가 겹치게 되었을때 불러올 다음 동물 데이터 함수 입니다.

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
//...
useEffect(() => {
const options = {
root: viewport.current, // viewport
threshold: 0,
};

const handleIntersection = (entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
loadItems(); // 데이터를 불러옵니다.
observer.unobserve(entry.target); // 기존 타겟을 unobserve 하고
observer.observe(target.current); // 데이터 변경된 새로운 카드 타겟을 observe 합니다.
});
};

const io = new IntersectionObserver(handleIntersection, options);

if (target.current) {
io.observe(target.current); // target
}

return () => io && io.disconnect();
}, [target, viewport]);
//...

intersection observer api로 infinifyScroll을 구현해보았습니다.

최종코드

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
import React, { useRef, useEffect, useState } from "react";
import "./App.css";

const App = () => {
const [datas, setData] = useState([
{ name: "기린", id: 0 },
{ name: "강아지", id: 1 },
{ name: "토끼", id: 2 },
{ name: "호랑이", id: 3 },
{ name: "사자", id: 4 },
]);
const viewport = useRef(null);
const target = useRef(null);

const loadItems = () => {
setData((prevState) => {
const animals = [
{ name: "고양이" },
{ name: "코끼리" },
{ name: "원숭이" },
{ name: "고라니" },
{ name: "기린" },
{ name: "표범" },
];
const id = prevState[prevState.length - 1].id;
const animalId = animals.map((animal, index) => {
return { ...animal, id: id + index + 1 };
});
return [...prevState, ...animalId];
});
};

useEffect(() => {
const options = {
root: viewport.current,
threshold: 0,
};

const handleIntersection = (entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
loadItems();
observer.unobserve(entry.target);
observer.observe(target.current);
});
};

const io = new IntersectionObserver(handleIntersection, options);

if (target.current) {
io.observe(target.current);
}

return () => io && io.disconnect();
}, [viewport, target]);

return (
<div className="wrapper">
<section className="card-grid" id="target-root" ref={viewport}>
{datas.map((animal, index) => {
const lastEl = index === datas.length - 1;
return (
<div
key={index}
className={`card ${lastEl && "last"}`}
ref={lastEl ? target : null}
>
<p>아이디: {animal.id}</p>
<p>이름:{animal.name}</p>
</div>
);
})}
</section>
</div>
);
};

export default App;

최종 스크롤

Share