무한스크롤과 Intersection Observer API , scroll event
- -
pokeAPI를 이용해 Next.js에서 React-query를 사용한 무한스크롤을 구현하며 깊게 알게된것과 새롭게 사용해본 것들을 써보자 한다.
무한스크롤은 프론트엔드에서 거의 필수적이라 해도 무방할 것 같다.
서버에서 가지고 있는 모든데이터를 첫렌더링 시 한번에 호출하는 것은 굉장히 비효율적이기에, 일정 이상 스크롤이 이동 했을 때 다음 데이터를 패칭한다.
무한스크롤을 구현할때는 데이터를 어떻게 나누어 보낼 것인지와 일정 이상 스크롤이 이동했을 때를 감지하여 요청을 보내는 것이 중요하다.
일정이상 스크롤이 이동했을 때를 감지하기 위해서는 다음과 같은 방법이 있다.
1. scroll event
스크롤 이벤트는 무엇보다 매우 쉽게 구현을 할 수 있다는 장점이 있다. 하지만 단독으로 사용시 브라우저 성능에 비효율적이다.
짧은 시간 간격으로 연속적으로 발생한다. 이를 함수에 등록해서 사용한다면..? 너무 잦은 호출을 유발하고, 이는 곧 성능저하를 유발할 수 있다. 동기적으로 작동한다
2. getBoundingClientRect()
뷰포트 대비 좌표를 얻어서 scroll event 구현이 가능하다.
const target = document.getElementById("here");
const clientRect = target.getBoundingClientRect();
const Top = clientRect.top;
getBoundingClientRect()는 뷰포트를 이루는 가장자리와 element간 거리 정보를 갖고있는 객체를 리턴한다.
top, bottom, left, right 프로퍼티를 리턴하는데 각 뷰포트와 방향별 거리를 의미한다.
왼쪽 상단을 기준으로 (0,0)이다.
하지만, 이는 reflow를 발생시킨다.
🤷🏻♀️ reflow : 렌더링 과정 중 레이아웃 계산을 다시하는 것을 의미한다. reflow가 발생하면 repaint는 필연적으로 다시 발생하게 된다. reflow는 레이아웃의 요소의 크기, 위치 등을 다시 계산해야하므로 시간이 오래걸린다.
reflow는 다음과 같은 경우 발생한다
- 요소의 크기 또는 위치의 변경
- 브라우저 창 크기 변경
- font size 변경
- scroll
- DOM API 통한 노드 요소 추가 및 삭제
3. Intersection Observer API
뷰포트와 설정요소의 교차점을 관찰한다. 요소가 뷰포트에 포함 되는지 아닌지를 관찰한다. 쉽게말하면 사용자가 보고 있는 화면에 요소가 있는지 없는지를 관찰한다.
Intersection Observer API 는 비동기적으로 실행되며 scroll 이벤트 기반요소에서 발생하는 렌더링 성능이나 이벤트 연속 호출 문제가 나타나지 않는다.
Intersection Observer 은 모든 영역을 사각형으로 취급한다. 만약 사각형이 아닐지라도 사각형이라 가정하고 교차성을 계산한다.
Intersection Observer 는 다음과 같이 new 생성자 함수를 통해 인스턴스를 생성하여 사용한다.
let observer = new IntersectionObserver(callback, options);
callback 은 교차성에 변화가 생겼을 때 실행되는 함수이다.
option은 콜백이 호출될 상황을 정한다.
option
root
가시성을 확인하는 요소이다. root = null 일때 viewport로 설정된다.
타켓의 부모요소로 설정해야한다
rootMargin
루트 요소의 범위를 넓히고자 할때 사용한다. css margin과 유사하게 사용할 수 있으며 단위요소를 필수로 입력해야한다.
threshold
콜백이 실행될 타켓요소의 가시성 퍼센티지를 나타내는 숫자 혹은 배열이 올 수 있다.
Callback
타겟 요소의 관찰이 시작되거나, 가시성의 변화가 감지되면 (threshold를 설정한 경우라면 설정한 퍼센티지 일때) 설정한 callback이 실행된다.
let callback = (entries, observer) => {
entries.forEach(entry => {
// 각 entry는 가시성 변화가 감지될 때마다 발생하고 그 context를 나타냅니다.
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
Entries
IntersectionObserverEntry 인스턴스를 담은 배열이다.
설정한 root 요소와 target요소의 교차 상황을 보여준다. 포함된 프로퍼티는 모두 읽기 전용이다.
이중 IntersectionObserverEntry.boundingClientRect 가 포함되어 있는데, 앞서 언급한 getBoundingClientRect()와는 다르게 reflow를 발생시키지 않는다.
Intersection Observer의 경우 타겟 요소의 관찰이 시작되면 콜백이 호출된다. 따라서 target-root 요소간 교차가 없음에도 콜백이 호출되는데, 이는 Intersection Observer의 기본동작이다. 이 기본동작을 막기 위해서는 intersectionRatio 를 사용할 수 있다.
let callback = (entries, observer) => {
entries.forEach(entry => {
// 타겟 요소가 루트 요소와 교차하는 점이 없으면 콜백을 호출하나, return 하지 않음
if (entry.intersectionRatio <= 0) return
});
};
IntersectionObserverEntry.isIntersecting은 target요소와 root요소가 교차하는지 boolean 값을 리턴해준다.
Method
- IntersectionObserver.observe(targetElement) : 타겟 요소에 대한 관찰을 시작.
- IntersectionObserver.unobserve(targetElement) : 타겟 요소에 대한 관찰을 중지. 관찰의 목적이 이루어져 굳이 계속 관찰을 할 필요가 없는 경우 사용.
- IntersectionObserver.disconnect() : 인스턴스의 타겟 요소들에 대한 모든 관찰을 중지.
- IntersectionObserver.takerecords(targetElement) : IntersectionObserverEntry 인스턴스들의 배열을 리턴.
MDN 문서에서는 Intersection Observer API 의 사용용례를 다음과 같이 말한다.
- 페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 레이지 로딩
- 스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll 을 구현
- 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고
- 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할 지 여부를 결정
Iinfinite-scroll
나는 Intersection Observer 를 react hook으로 만들어 사용했다.
import { useEffect } from "react"
interface useIntersectionObserverProps {
root?: null;
rootMargin?: string;
threshold?: number;
target: any;
onIntersect:any;
enabled: boolean | undefined;
}
export default function useIntersectionObserver({
root,
target,
onIntersect,
threshold = 1.0,
rootMargin = '0px',
enabled = true,
}: useIntersectionObserverProps) {
useEffect(() => {
if (!target) {
return;
}
const observer = new IntersectionObserver(
entries =>
entries.forEach(entry => entry.isIntersecting && onIntersect()),
{
root: root,
rootMargin,
threshold,
}
)
const el = target && target.current
if (!el) {
return
}
observer.observe(el)
return () => {
observer.unobserve(el)
}
}, [target, enabled, root, threshold, rootMargin, onIntersect])
}
entry.isIntersection 이 true일때, 즉 ref로 찍어준(=target)요소와 root가 교차할때 fetch 함수가 실행된다.
react- query를 사용했기에 callback함수에 다음페이지를 호출하는 함수(=fetchNextPage)를 넘겨주었다.
useIntersectionObserver({
root: null,
target: endLine,
onIntersect: fetchNextPage,
enabled: hasNextPage,
})
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,} = useInfiniteQuery(["InfinitePokemon"], getAllPokemon, {
getNextPageParam: (lastPage) => {
const nextpage = lastPage.next;
if(!nextpage) return false;
return Number(new URL(nextpage).searchParams.get("offset"));
}
})
ref
https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
'개발일기 > TIL' 카테고리의 다른 글
트러블슈팅이라하기엔 너무 민망한....이벤트 버블링 (0) | 2023.03.20 |
---|---|
JWT - Access Token/ Refresh Token (1) | 2023.02.06 |
CRA(Create react-app)/ Vite 무엇이 다른가? (2) | 2023.01.14 |
Base64 인코딩과 이미지업로드 회고.... (0) | 2022.12.26 |
[TIL] React 심화 주차 키워드 (1) | 2022.09.01 |
소중한 공감 감사합니다