목표 : 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 함수의 경우 이상 동작
- 함수의 코드 순서를 확인해 보자
- throttle 함수 ver1
function throttle(callback, delay = 3000) { let wait = false; return (...args) => { if (wait) { return; } callback(...args); wait = true; setTimeout(() => { wait = false; }, delay); }; }
- 이상동작 원인
- 일단 스크롤 이벤트가 생길 때 마다 callback 함수가 실행되는데,
- 스크롤 이벤트가 트리거 되는 시작점의 스크롤 위치 값을 들고 감
- if (window.innerHeight + scrollTop >= offsetHeight) {
- 그래서 스크롤 바닥 조건에 걸리지 않음
- fetch 함수를 실행하지 않음
- 함수의 코드 순서를 확인해 보자
- 화면 예시
- 한번에 끝까지 스크롤 한 상태
- 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 : 대상이 되는 객체의 가시성을 확인 할 때 사용하는 뷰포트 요소, 기본값은 브라우저 뷰포트
- options 값 커스텀
-
-
- rootMargin : root 가 가진 여백, 교차성을 개산하기 전에 적용됨
-
-
-
- threshold : observer의 콜백함수가 실행될 대상 요소의 가시성 퍼센테이지, 10%만 보여도 수행하는가?, 100% 다보였을때 실행하는가
-
3. 관찰대상 지정하기
- 요소를 observer.observe 메소드에 등록해주면 된다
let target = document.querySelector('#listItem');
observer.observe(target);
4. 콜백함수 구현하기
- 콜백함수 기본 args
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 객체 내부 key
-
- 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)
})
참고
- http://blog.hyeyoonjung.com/2019/01/09/intersectionobserver-tutorial/
- https://tech.kakaoenterprise.com/149
'programming > React' 카테고리의 다른 글
useEffect 마운트 되었을때는 실행하지 않고, dependancies 가 실제로 변경된 경우에만 실행 시키기 (0) | 2023.11.15 |
---|---|
blob/json 형태로 리턴받은 데이터를 엑셀 파일로 추출하기(Get 방식 /Post 방식) (0) | 2023.11.14 |
개발환경에서 CORS 에러 관련 프록시 설정 (0) | 2023.05.03 |
Recoil 새로고침 시 user 데이터 유지하기 (0) | 2023.05.03 |
리액트 빌드시 환경(개발/테스트/상용)에 따라 API 주소 설정하기 (0) | 2022.04.04 |