이번 프로젝트에서 스터디 일지부분을 무한스크롤을 통해 원하는 게시글 수만큼을 추가로 불러오고 싶다.
이전에 사용했던 방법은 react-custom-scrollbars-2에서 제공하는 onScrollFrame
이라는 속성을 사용해 스크롤 위치가 top인지를 확인하는 방법으로 데이터를 불러왔었다. ( throttle 조차 사용하지 않음)
하지만 이러한 방법은 브라우저 성능을 저하시킬 수 있다는 얘기가 있어 Intersection Observer API
를 사용해 무한스크롤을 구현해보고자 한다.
👇 아래는 Intersection Observer API 사용방법이다.
https://velog.io/@chacha_fe/Intersection-Observer-API
useObserver 커스텀훅 구현
import { useEffect, useRef, useState } from "react";
type Callback = () => void;
const useObserver = (callback: Callback) => {
const targetRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!targetRef.current) return;
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (
entry.isIntersecting &&
entry.intersectionRatio === 1 &&
!loading
) {
setLoading(true);
callback();
setLoading(false);
}
});
},
{
root: targetRef.current?.parentElement,
threshold: [1],
}
);
io.observe(targetRef.current);
return () => {
io.disconnect();
};
}, [callback, loading]);
return [targetRef];
};
export default useObserver;
callback
함수를 인자로 받는 useObserver.ts를 생성targetRef
를 생성loading
state를 생성isIntersecting = true
(교차 상태)이며 intersectionRation = 1
(100% 가시되고 있으며) loading = false
(useObserver가 실행되지 않고 있다면) callback
을 실행한다.root
대상 요소의 부모 요소이다. io.observe(targetRef.current)
를 통해 대상요소를 관측한다. targetRef
를 리턴한다.useObserver.ts 사용
const scrollRef = useRef<Scrollbars>(null);
const [page, setPage] = useState(0);
const [diarys, setDiarys] = useState<Diarys>([]);
const [scrollHeight, setScrollHeight] = useState<number>(0);
const getPage = useCallback(() => {
if (!scrollRef.current) return;
setPage((prev) => prev + 1);
}, []);
const [targetRef] = useObserver(getPage);
...
return (
****<Scrollbars autoHide ref={scrollRef}>
<div ref={targetRef} />
<div className="flex flex-col px-[3rem] ">
{Object.entries(DiarySections || {})?.map(([day, diarys]) => (
<div
key={day}
className="flex flex-col space-y-[2.8rem] border-b py-[2.8rem] last:border-none"
>
<span className="flex-center Sub2 w-[11.6rem] rounded-full bg-primary-500 py-[0.6rem] text-primary-100">
{day}
</span>
<ul className="flex flex-col space-y-[1.4rem]">
{diarys?.map((diary) => (
<DiaryItem key={diary.userId} diary={diary} />
))}
</ul>
</div>
))}
</div>
</Scrollbars>
)
const [targetRef] = useObserver(getPage);
스크롤을 가장 위로 올렸을 때 page를 1씩 더해주는 함수 getPage
를 인자로 전달한다.
<Scrollbars autoHide ref={scrollRef}>
<div ref={targetRef} />
...
</Scrollbars>
감시할 대상에게 targetRef
를 적용시킨다.
감시할 대상의 부모 요소인 Scrollbars
가 root 요소가 된다. 스크롤을 올려 감시 요소가 전부 보여졌을 때
useObserver
에 전달한 getPage
가 실행된다.
❓
한 가지 문제점이 있다. 스크롤의 가장 위로 올라갔을 때 데이터가 추가된다면 스크롤의 위치가 들어온 데이터만큼 내려와야 하는데 최상단에 가만히 멈춰있다. 이를 해결하기 위해서는 추가된 데이터들의 높이만큼 스크롤 계산을 해주어야 한다.
스크롤 계산하기
스크롤이 root 요소의 가장 최상단에 닿았을 때 즉, 감시 요소가 전부 보여졌을 때 page
를 1씩 증가시키는 함수를 callback으로 전달하였다. 이때 현재 root의 높이를 저장한다.
const getPage = useCallback(() => {
if (!scrollRef.current) return;
setPage((prev) => prev + 1);
// 현재 root의 높이
setScrollHeight(scrollRef.current.getScrollHeight());
}, []);
데이터가 추가된 후에 root의 높이 - 저장한 root의 높이만큼 스크롤 최상단에서 내려오게한다.
useEffect(() => {
// 첫 마운트 되었을 때 실행시키지 않게 하기 위해서 처리
if (DiarySections.length <= 10 || !scrollRef.current) return;
// 데이터가 들어온 후에 root 높이
let currentHeight = scrollRef.current.getScrollHeight();
// 이전과 현재의 root 높이가 같다면 실행 x (굳이 필요할까 싶기도하다.)
if (scrollHeight === currentHeight) return;
//scroll 위치를 root 최상단에서 현재 - 이전 높이만큼 멀어지게한다.
scrollRef?.current.scrollTop(currentHeight - scrollHeight);
// 데이터가 변경될 때마다 실행
}, [DiarySections]);