
개인적으로 뉴스 뷰어를 만들게 되었는데, 무한스크롤 구현에서 삽질을 너무 많이 해서 힘들었다. 결론적으로 구현 초기에는 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을 동적으로 변경할 수 있도록 했다.