JSONPlaceholder 의 api로 테스트
https://jsonplaceholder.typicode.com/posts
100개의 데이터가 있고 쿼리스트링으로 접근하면 start = 몇번부터 limit= 몇개 나눠서 데이터를 가져올 수 있다.
window.innerHeight + window.scrollY >= document.body.offsetHeight
useRef로 리렌더링을 일으키지않고 상태값 유지
이벤트 발생 시점에 fetch가 데이터를 받아오는동안 onScroll 이벤트발생을 막을 toggle 사용
const InfiniteScroll = () => {
const [feed, setFeed] = useState([]);
const add = useRef(0);
const [toggle, setToggle] = useState(true);
const onScroll = () => {
if (
window.innerHeight + window.scrollY >= document.body.offsetHeight &&
toggle
) {
add.current += 10;
setToggle(prev => !prev); --> 추가 이벤트가 발생되지 않도록 false 전환
fetch(
`https://jsonplaceholder.typicode.com/
posts?_start=${add.current}&_limit=10`
)
.then(response => response.json())
.then(data => {
setFeed(prev => [...prev, ...data]);
setToggle(prev => !prev);
// fetch로 받아온 데이터 처리를 다 마치고 true 전환
});
}
};
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts?_start=0&_limit=10`)
.then(response => response.json())
.then(setFeed);
}, []);
useEffect(() => {
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
});
return feed?.map(({ id, title, body }, idx) => (
<div key={idx}>
<div>{id}</div>
<h1> {title}</h1>
<div>{body}</div>
</div>
));
};
소스 코드입니다. 복사해서 확인해보시면 글로 설명하는것보다 더 쉽게 이해될 것입니다.
https://github.com/kimjuno97/React-Skill-Repository/blob/master/src/무한스크롤/InfiniteScroll.js
IntersectionObserver
참고 자료
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
https://flamingotiger.github.io/frontend/react/intersection-observer-api/
-> 자세한 설명은 여기서 보세요 잘 나와있습니다. 해당 자료를 참고하여 데이터를 fetch로 받아 올때로
적용해 보았습니다.
threshold 값을 조절하여 데이터 받아오는 시점을 제어 한다.
export default function Observer() {
const [datas, setData] = useState([]);
const [toggle, setToggle] = useState(false);
const add = useRef(0);
const viewport = useRef(null); // section 그자체 가져옴
const target = useRef(null); // 참조가 늦어짐. console 찍으면 null
const loadItems = () => {
console.log('한번만 발동');
add.current += 10;
setToggle(prev => !prev);
fetch(
`https://jsonplaceholder.typicode.com/posts?_start=${add.current}&_limit=10`
)
.then(response => response.json())
.then(data => {
setData(prev => [...prev, ...data]);
setToggle(prev => !prev); // fetch로 받아온 데이터 처리를 다 마치고 true 전환
});
};
useEffect(() => {
console.log('이펙트발생');
const options = {
root: viewport.current,
threshold: 0.5,
};
const handleIntersection = (entries, observer) => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
return;
}
if (toggle) 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, toggle]);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts?_start=0&_limit=10`)
.then(response => response.json())
.then(data => {
setData(data);
setToggle(prev => !prev);
// target 참조가 처음에 바로 안되서 위의 useEffect가 발생하지않는다.
// 리렌더링 시킨다.
});
}, []);
return (
<div className="wrapper">
<section className="card-grid" id="target-root" ref={viewport}>
{datas?.map(({ title, body, id }, index) => {
const lastEl = index === datas.length - 1;
return (
<div
key={index}
className={`card ${lastEl && 'last'}`}
ref={lastEl ? target : null}>
<div>{id}</div>
<h1>{title}</h1>
<p>{body}</p>
</div>
);
})}
</section>
</div>
);
}
css파일
.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;
}
소스코드입니다. 복사해서 확인해보시면 글로 설명하는것보다 더 쉽게 이해될 것입니다.
https://github.com/kimjuno97/React-Skill-Repository/blob/master/src/무한스크롤/Observer.js
참고용 글이라 설명이 없는 점 양해 바랍니다. 질문 주시면 답변 꼭 해드리겠습니다.
IntersectionObserver 2022 12/02 추가
IntersectionObserver를 많이 사용하게 되면서 웹에서 한번만 실행되면 된다는 것을 깨달았다.
방법은 2가지만 지키면된다.
위의 방법보다 아래의 방법을 쓰는 것이 훨씬 최적화에 좋은 코드 입니다.
export default function InfiniteScroll() {
const [feed, setFeed] = useState<TypeFeed[]>([]);
const target = useRef<HTMLDivElement | null>(null);
console.log('feed 증가 확인', feed);
useEffect(() => {
let io: IntersectionObserver | null = null;
if (target.current) {
io = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setTimeout(() => {
setFeed((prev: TypeFeed[]) => [...prev, ...data]);
}, 500);
}
});
});
io.observe(target.current);
}
}, []);
if (feed.length === 0) {
return (
<Container>
<Loading>Loading...</Loading>
<div ref={target} />
</Container>
);
}
return (
<div>
{feed.map(({ img }, idx) => {
return <ImgBox key={idx} img={img} />;
})}
<div ref={target} /> // Ref를 컨텐츠 제일 아래에
</div>
);
}