목표 : React 에서 무한 스크롤 구현하기 (Infinity Scroll in React)
여정 : scroll Event Listener -> Throttle 로 최적화 -> Intersection Observer API 사용
1. 단순 구현
스크롤 이벤트를 참조하여 다음 데이터 API 호출
코드 구현
가정
fetchData() 는 API 를 요청하는 함수
예제에서는 console.log('fetch API 요청') 찍히는 걸로 API 요청을 대신함
useEffect(() => {
const handleScroll = () => {
const { scrollTop, offsetHeight } = document.documentElement;
if (window.innerHeight + scrollTop >= offsetHeight) {
// 스크롤 위치 바닥일때 데이터 페치
fetchData() // -> API 요청하는 함수
console.log('fetch API 요청');
}
};
// 화면 진입시 데이터 페치
fetchData() // -> API 요청하는 함수
console.log('fetch API 요청');
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
개선 포인트
documentElement.scrollTop과 documentElement.offsetHeight는 리플로우(Reflow)가 발생하는 참조이므로 API를 과다로 요청하는 경우 발생 가능
2. 쓰로틀 (Throttle) 적용
마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
동일 이벤트가 반복적으로 시행되는 경우 이벤트의 실제 반복 주기와 상관없이 임의로 설정한 일정 시간 간격(밀리세컨드)으로 콜백 함수의 실행을 보장
코드 구현
기본 Throttle 함수(ver1)
function throttle(callback, delay = 300) {
let waiting = false
return (...args) => {
if (waiting) return
callback(...args)
waiting = true
setTimeout(() => {
waiting = false
}, delay)
}
}
callback: 실행 대상이 되는 콜백 함수
delay : 얼마 간격으로 함수를 실행할 지 결정하며 millisecond 단위, 기본값으로 0.3초를 주었음
waiting 상태가 true 인 경우 실행 없음
최초 함수가 호출되었을 때 waiting은 false
이 때 콜백 함수를 실행한 뒤 다시 waiting은 true가 됩니다.
waiting이 true가 되었을 때 delay 밀리초 후에는 waiting이 강제로 false가 되고, 다시 콜백 함수가 실행이 됩니다.
1-2. 기본 Throttle 함수(ver2)
function throttle(callback, delay = 3000) {
let timer;
return (...args) => {
if (timer) return;
timer = setTimeout(() => {
callback(...args);
timer = null;
}, delay);
};
}
timer 를 생성하는 역할
생성해둔 timer 가 있으면, 콜백함수 실행 없이 return
timer 가 없으면, timer 를 만듦
만든 timer 는 delay 시간 이후에 콜백 함수를 실행하는 타이머
⇒ 두가지 throttle 함수를 각각 적용해봤을 때 동작에 차이가 발생했음
2. handleScroll 함수에 throttle 함수를 적용
useEffect(() => {
const handleScroll = throttle(() => {
const { scrollTop, offsetHeight } = document.documentElement;
if (window.innerHeight + scrollTop >= offsetHeight) {
// 스크롤 위치 바닥일때 데이터 페치
console.log('fetch API 요청');
}
});
// 화면 진입시 데이터 페치
console.log('fetch API 요청');
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
ver2 throttle 함수의 경우 : 정상 동작
스크롤 바닥
3000ms 대기
timer (setTimeout) 생성
callback 함수 실행
timer 해제
스크롤 위로 갔다가 다시 바닥
3000ms 대기
…
ver1 throttle 함수의 경우 이상 동작
화면 예시
한번에 끝까지 스크롤 한 상태
callback 함수가 호출됨
scrollTop 위치를 “3” 으로 물고 있음
fetch 할 조건 (scrollTop이 아래에 위치해 있는가?) 에 해당되지 않아서 fetch 함수가 실행되지 않는다
delay 시간 이후에 callback 함수를 실행해야 원하는 scrollTop 값으로 조건체크를 할 수 있음
function throttle(callback, delay = 3000) {
let wait = false;
return (...args) => {
if (wait) {
return;
}
wait = true;
setTimeout(() => {
callback(...args);
wait = false;
}, delay);
};
}
delay 시간 이후 callback 함수가 호출 되어서 최신 scrollTop 위치값을 가져올 수 있
⇒ 스크롤 이벤트 특성상 delay 이후에 콜백함수가 실행되는 것이 정상 동작이었으나, 케이스에 따라 콜백을 먼저 실행하고, delay 가 있어야 하는 경우도 있을 것
3. Intersection Observer API (체신 문법 적용^^)
Intersection Observer API 적용
교차 관찰자 API
브라우저에서 제공하는 API
크롬 51버전부터 사용가능
위에서 확인했듯이, 스크롤 이벤트는 일단 스크롤이 발생하면 트리거됨
우리가 원하는 것은 스크롤이 아래에 도달했을 때만 트리거 시키고 싶음
코드 구현
new IntersectionObserver() 생성자 표현식을 사용하여, observer 객체를 생성하여 사용
생성
교차될때 실행할 콜백함수를 제공해야 한다 → fetchData 함수
options 로 root / rootMargin / threshold 값을 커스텀 할 수 있음
설정
options 값 커스텀
root : 대상이 되는 객체의 가시성을 확인 할 때 사용하는 뷰포트 요소, 기본값은 브라우저 뷰포트
rootMargin : root 가 가진 여백, 교차성을 개산하기 전에 적용됨
threshold : observer의 콜백함수가 실행될 대상 요소의 가시성 퍼센테이지, 10%만 보여도 수행하는가?, 100% 다보였을때 실행하는가
3. 관찰대상 지정하기
요소를 observer.observe 메소드에 등록해주면 된다
let target = document.querySelector('#listItem');
observer.observe(target);
4. 콜백함수 구현하기
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
IntersectonObserverEntry 리스트를 첫번째 인자로 받고, observer 자체를 두번째 인자로 받는다
React 용 useIntersect hook
const { useRef, useCallback, useEffect } = require('react');
const useIntersect = (onIntersectFn, options) => {
const ref = useRef(null); // 1
const callback = useCallback(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) onIntersectFn(entry, observer);
});
},
[onIntersectFn]
);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(callback, options); // 2
observer.observe(ref.current); // 3
return () => observer.disconnect();
}, [ref, options, callback]);
return ref;
};
export default useIntersect;
관찰 타겟 ref 생성
observer 생성
observer.observe 에 관찰 타겟 ref 연결
ref unmount 시 observer disconnect 시키기 (삭제)
callback 함수 선언
entry 가 isIntersecting true 인터섹트 트리거 되면,
onIntersectFn 함수를 실행
onIntersectFn은 useIntersect 훅 선언시 전달 → fetchData 함수
useIntersect 훅 사용부분
핵심 내용은 fetchData 함수를 실행해야함
타겟에 대한 관찰을 멈출지는 상황에 따라 판단
const ref = useIntersect(async (entry, observer) => {
// observer.unobserve(entry.target);
// if(hasNextPage && !isFetching) {
// console.log('fetchNextPage');
// }
console.log('fetchNextPage');
});
4. 폴리필
safari 에서 지원하고 있지 않음 폴리필 필요
https://github.com/GoogleChromeLabs/intersection-observer
https://github.com/w3c/IntersectionObserver
$ npm i intersection-observer
import 'intersection-observer'
const io = new IntersectionObserver(콜백함수, 옵션)
const 관찰대상 = document.querySelectorAll(관찰대상)
// IE에서 사용하기
Array.prototype.slice.call(관찰대상).forEach(elim => {
io.observe(elim)
})
참고