Intersection Observer로 요소 감지하기로 Intersection Observer API
를 익힌 후 무한 스크롤도 구현해 보았다.
import type { NextPage } from "next";
const ListItem = ({ data }: { data: string }) => {
return (
<div>
<div
style={{
textAlign: "center",
lineHeight: 5,
fontSize: "2rem",
border: "1px solid black",
height: 200
}}
>
{data}
</div>
</div>
);
};
const fakeFetch = (delay = 1000) =>
new Promise((res) => setTimeout(res, delay));
const currentData = [
"apple",
"banana",
"orange",
"lemon",
"lime",
"pure",
"peach",
"berry"
];
const nextItem = [
"dorian",
"mango",
"starfruit",
"dragonFruit",
"almond",
"walnut",
"grape",
"persimmon"
];
const Home: NextPage = () => {
const [state, setState] = useState<{ item: string[]; isLoading: boolean }>({
item: [...currentData],
isLoading: false
});
const { item, isLoading } = state;
return (
<div>
{item.map((fruit, i) => {
return <ListItem key={i} data={fruit} />;
})}
<div>
{isLoading && (
<div
style={{
textAlign: "center",
lineHeight: 5,
fontSize: "2rem",
border: "1px solid black",
height: 200,
background: "#eee"
}}
>
Loading...
</div>
)}
</div>
</div>
);
};
export default Home;
임시 데이터로 두 개의 배열을 준비했다. currentData
가 처음 렌더링되고 이후 스크롤이 끝날 때마다 fakeFetch
로 1초 기다린 후 nextItem
이 렌더링된다.
감지할 요소는 로딩 영역이다. 스크롤 마지막에 존재하며, 감지될 경우 데이터를 패치한다. 로딩 영역에 ref
를 달고 관찰한다.
const target = useRef<HTMLDivElement>(null);
useEffect(() => {
let observer: IntersectionObserver;
if (target) {
observer = new IntersectionObserver(
async ([e], observer) => {
if (e.isIntersecting) {
observer.unobserve(e.target);
await fetchItems(nextItem);
observer.observe(e.target);
}
},
{ threshold: 1 }
);
observer.observe(target.current as Element);
}
return () => observer.disconnect();
}, [target]);
데이터가 패치되는 동안에는 관찰할 필요가 없으므로 unobserve
하고, 패치 후 다시 observe
한다.
const fetchItems = async (nextItem: string[]) => {
setState((prev) => ({
...prev,
isLoading: true
}));
await fakeFetch();
setState((prev) => ({
item: [...prev.item, ...nextItem],
isLoading: false
}));
};
가짜 데이터를 패치하는 함수는 이러하다.
import type { NextPage } from "next";
import { useEffect, useRef, useState } from "react";
const ListItem = ({ data }: { data: string }) => {
return (
<div>
<div
style={{
textAlign: "center",
lineHeight: 5,
fontSize: "2rem",
border: "1px solid black",
height: 200
}}
>
{data}
</div>
</div>
);
};
const fakeFetch = (delay = 1000) =>
new Promise((res) => setTimeout(res, delay));
const currentData = [
"apple",
"banana",
"orange",
"lemon",
"lime",
"pure",
"peach",
"berry"
];
const nextItem = [
"dorian",
"mango",
"starfruit",
"dragonFruit",
"almond",
"walnut",
"grape",
"persimmon"
];
const Home: NextPage = () => {
const target = useRef<HTMLDivElement>(null);
const [state, setState] = useState<{ item: string[]; isLoading: boolean }>({
item: [...currentData],
isLoading: false
});
const fetchItems = async (nextItem: string[]) => {
setState((prev) => ({
...prev,
isLoading: true
}));
await fakeFetch();
setState((prev) => ({
item: [...prev.item, ...nextItem],
isLoading: false
}));
};
useEffect(() => {
let observer: IntersectionObserver;
if (target) {
observer = new IntersectionObserver(
async ([e], observer) => {
if (e.isIntersecting) {
observer.unobserve(e.target);
await fetchItems(nextItem);
observer.observe(e.target);
}
},
{ threshold: 1 }
);
observer.observe(target.current as Element);
}
return () => observer.disconnect();
}, [target]);
const { item, isLoading } = state;
return (
<div>
{item.map((fruit, i) => {
return <ListItem key={i} data={fruit} />;
})}
<div ref={target}>
{isLoading && (
<div
style={{
textAlign: "center",
lineHeight: 5,
fontSize: "2rem",
border: "1px solid black",
height: 200,
background: "#eee"
}}
>
Loading...
</div>
)}
</div>
</div>
);
};
export default Home;
참고
React - Intersection Observer API를 사용하여 인피니트 스크롤 구현하기
Intersection Observer API의 사용법과 활용방법