개인적으로 뉴스 뷰어를 만들게 되었는데, 무한스크롤 구현에서 삽질을 너무 많이 해서 힘들었다. 결론적으로 구현 초기에는 Scroll Event
를 이용하여 구현을 시도하였으나, 여러 문제에 봉착하게 되었다.😢
어떠한 문제가 있었냐면,,,,(물론 내가 구현을 잘못해서 문제가 발생한 것이다...)
스크롤이 바닥에 닿았을 때, 닿았다는 인식은 잘한다.
하지만 닿고 나서 axios로 서버에서 데이터를 가져올 때, 그 가져오는데 걸리는 시간 동안 스크롤은 계속 닿아있기 때문에 page를 저장하는 state를 1 씩 증가하길 원했으나 엄청나게 급격하게 증가하는 경우 발생.
다른 방법을 알아본 결과 Intersection Observer API
를 알게 되었고, 공부하여 무한스크롤을 구현하게 되었다. 잊어버리지 않기 위해 기록한다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
MDN에서는 위와 같이 말하고 있다.
Intersection Observer API로 관찰하는 타겟 요소가 자신의 상위 요소와 교차되는지 관찰하고 있다가 교차되면 어떠한 동작을 취할 수 있는 기능을 제공해준다고 나는 이해를 했다.
페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩
스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll 을 구현.
광고 수익을 계산하기 위한 용도로 광고의 가시성 보고.
사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할 지 여부를 결정.
MDN에서 나열한 사용 케이스들인데, 무한스크롤 뿐만 아니라 지연 로딩을 구현하는데도 사용할 수 있다니 좀 유용한 것 같다. 일단 나는 무한스크롤이 더 중요하다.
위의 그림을 설명하면 반복되는 List가 있고, 마지막 List는 스크롤이 아직 끝까지 되지 않아 뷰포트 밖에 있다. 이 마지막 List가 Target 즉, 관찰 대상이다.🔥
Target이 뷰포트 밑에 닿았을 때, 새로운 데이터를 불러와 더하여 무한스크롤을 구현하는 아이디어다.
👉 또한, 무한스크롤되어 새로운 데이터가 기존 데이터에 합쳐지면 마지막 List가 바뀌기 때문에 유동적으로 Target을 변경해야한다.
let observer = new IntersectionObserver(callback, option)
IntersectionObserver 생성자는 callback함수와 option을 파라미터로 받는다.
callback(entries, observer)
callback함수는 target과 root가 교차 되었을 때 실행할 함수를 뜻한다.
root가 무엇인가❓ option 안에 있다. option은 3-2. 에서 설명하겠다.
entries는 IntersectionObserverEntry 인스턴스의 배열이다.
이 배열은 읽기 전용의 다음 속성들을 포함한다.
boundingClientRect : 관찰 대상의 사각형 정보
intersectionRect : 관찰 대상의 교차한 영역 정보
intersectionRatio : 관찰 대상의 교차한 영역 백분율
isIntersecting : 관찰 대상의 교차 상태 boolean타입
time : 변경이 발생한 시간 정보
target : 관찰 대상 요소
rootBounds : 지정한 루트 요소의 사각형 정보
option은 객체인데, observer 콜백이 호출되는 상황을 조정할 수 있다.
option은 root, rootMargin, threshold 이렇게 세 개의 값을 가진다.
대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.
MDN에서 이렇게 말하고 있다.
즉, 관찰 대상(target)과 기준이 되는 대상(root)가 교차되는 것을 관찰하는게 Intersection Observer API이다.
root 가 가진 여백입니다. 이 속성의 값은 CSS의 margin 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.
observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.
기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.
threshold는 위 그림과 같이 이해하면 될 것 같다.
const NewsItem: React.FC<NewsItemProps> = ({ item, setPage, index, length }) => {
const [target, setTarget] = useState<HTMLDivElement | null>(null);
const onIntersect = ([entry]: IntersectionObserverEntry[], observer: IntersectionObserver)=>{
if (entry.isIntersecting) {
setPage(page => page + 1)
// 데이터를 불러오고 화면에 렌더링 되기 전까지 계속 요건을 충족하므로
// page가 무한대로 증가하는 경우 방지
// 기존의 target을 unobserve함으로써 page가 한 번만 증가하게끔 함
observer.unobserve(entry.target);
}
}
useEffect(() => {
let observer: IntersectionObserver;
if (target) {
observer = new IntersectionObserver(onIntersect, { threshold: 0.3 });
observer.observe(target);
}
return (() => {
observer && observer.disconnect();
})
}, [target])
//...(생략)
return (
<>
<div className='news-item-wrapper' onClick={clickHandler}
ref={index === length - 1 ? setTarget : null}>
<img src={item.urlToImage} alt="NewsThumbnailImage" />
<div className="news-item-body">
<div className='article-info'>
<h1>{item.title}</h1>
<p className='news-item-content'>{item.content}</p>
</div>
<div className='news-info'>
<p>Author : {authorRename(item.author)}</p>
<p>Published : {slicePublishedAt(item.publishedAt)}</p>
<p>Source : {item.source.name}</p>
</div>
</div>
<div className='star-area'>
<p className='star'>☆</p>
</div>
</div>
</>
)
target과 root가 겹쳐지면 실행할 콜백함수 onIntersect
에서 entry 중 isIntersecting이 true값이면 page를 1 증가시키고 관찰을 잠시 취소한다(unobserve). 취소하는 이유는 위 코드에 주석으로 설명을 해놓았다.
page를 1 증가하는 이유는 위 코드에서는 생략했지만 내가 사용한 API에서 한 페이지당 20개의 데이터를 불러오는데, 그 페이지를 증가시켜 다른 페이지의 데이터를 불러오기 위해 증가시킨다.
target이 변경되면 useEffect가 실행되는데, target이 존재하면 IntersectionObserver 객체를 만들고 원하는 target을 등록한다.(observe) 그리고 컴포넌트가 언마운트 될 때는 반드시 disconnect를 해주어야 한다.
관찰 하는 대상을 유동적으로 변경해주어야하기 때문에
<div className='news-item-wrapper' onClick={clickHandler}
ref={index === length - 1 ? setTarget : null}>
아래와 같은 코드를 작성하여 target을 동적으로 변경할 수 있도록 했다.