useRef,IntersectionObserver을 이용하여 Infinityscroll을 만들어보자.
| topics | 201 React |
| references | |
| types | 실습 |
라이브러리를 쓰지말고 직접 infinity scroll을 구현해보자
어떻게 구현할까?
1. scrollEvent
scrollEvent를 사용하게되면 스크롤이 발생할 때마다 함수가 호출된다. throttling으로 이를 좀 더 개선시킬 수는 있으나 역시나 최선의 방법은 아니다.
2. intersectionObserver
const io = new IntersectionObserver((entries, observer) => {}, options)
io.observe(element)
IntersectionObserver API는 해당요소가 최초로 관측되거나 특정 요소와 교차할 때 호출된다. option을 통해 콜백이 호출되는 상황을 조작할 수 있다.
option
- root : 해당 요소를 기준으로 관측하고, 교차됨을 감지한다. default로 viewport가 지정된다.
- rootMargin : 이를 통해 root의 범위를 확장하거나 축소할 수 있다.
- threshold : 해당요소가 얼마나 보였을 때 콜백을 호출하는지 조절 가능하다
0 -> 보이자말자
[0,0.5,1] -> 보이자 말자, 절반 보일때, 완전히 다 보일때 콜백이 호출된다.
callback
- entries : entries는 교차점 관찰자들을 배열로 담고 있습니다. 읽기만 가능하다.
<해당 기능을 위해 쓰는 속성>
isIntersecting : 관측하는 요소이 기준요소와 교차되는가를 bool로 판단
여기에 쓰지않지만 entries의 자세한 속성들은 아래링크에서 설명되어있다.
https://heropy.blog/2019/10/27/intersection-observer/
메서드
- observe(el) : 해당요소 관찰시작
- unobserve(el) : 해당요소 관찰 중지
- disconnect() : 인스턴트가 관찰하는 모든요소 관찰 중지
구현방법!!
1. 페이징 api 처리를 위한 커스텀 훅을 제작해보자
- backend에서 돌려주는 응답값
{
"data": [
{
....
}
],
"next": 0
}
next부분에 이후 페이징 될 페이지를 응답해준다.
그리고 그 값을 다시 get요청의 파라미터로 넣어 다음페이지를 받는 구조로 되어 있었다.
Custom hook 제작
💡 customhook을 이용한 이유
인피니티의 복잡한로직을 구분하고 여러군대 재사용이 가능한 로직이기에 분리하였다
커스텀훅 전체코드
코드에 주석을 자세히 달아놓았으니 주석을 참고하길 바란다.
import { useEffect, useState } from 'react';
import diaryService from '../services/diary_api';
const useHomePosts = (nextPage) => {
//받아야할 데이터리스트들이다.
const [diaryList, setDiaryList] = useState([]);
//다음페이지를 담고있는 state이다
const [next, setNext] = useState(null);
//데이터를 주고 받는 상태를 표시하는 state이다
const [error, setError] = useState(null); //에러있으면 string ,아니면 null
const [loading, setLoading] = useState(false);//로딩중이면 true ,아니면 false
const sendQuery = () => {
console.log('sendQueryStart', nextPage, diaryList);
//로딩중임을 설정
setError(null);
setLoading(true);
//api호출
diaryService
.getFollowingDiary(nextPage)
.then((res) => {
//데이터가 있으먄
if (res?.data.data) {
setDiaryList((prev) => {
//이전다이어리에 현재 다이어리 값을 추가한다.
let list = [...prev, ...res.data.data];
//같은 id값을 필터링하는 함수
const filteredData = list.reduce(function (acc, current) {
if (
// 만약 같은아이디가 없으면
acc.findIndex(
(e) => parseInt(e.diaryId) === parseInt(current.diaryId),
) === -1
) {
//추가
acc.push(current);
}
//아니면 그냥 반환
return acc;
}, []);
list = filteredData;
return list;
});
setNext(res.data.next);//다음 페이지를 api에서 전달해준 정보로 바꾼다
} else {
//데이터가 안불러 와 졌으면 error 설정
setError('일기를 불러오는데 실패했습니다.');
}
//데이터 처리가 끝났으므로 로딩 false설정
setLoading(false);
})
.catch((e) => {
//데이터가 안불러 와 졌으면 error 설정 , 로딩 false설정
setError(e);
setLoading(false);
});
};
//다음페이지가 바뀌면 sendQuery를 실행한다.
useEffect(() => {
sendQuery(nextPage);
}, [nextPage]);
return [diaryList, next, error, loading];
};
export default useHomePosts;
2. IntersectionObserver을 이용하여 마지막 컴포넌트에 도달하였는지 인식하고 새로운 데이터 받기
useRef를 이용하여 컴포넌트안에서 조회 수정하는 변수를 관리할 수 있다
따라서 여기서는 ref.current에 IntersectionObserver객체를 지정를 지정하여 해당 노드가 뷰포트와 교차되는지 관찰하였다.
🌊 flow
- 현재 페이지를 0으로 지정하고 커스텀훅에 현재페이지를 인자로 넣어준다.
- 데이터의 로딩상태에 따라 콜백이 실행되는 useCallback을만들어준다.
- 로딩이거나,노드가 없거나, 다음 페이지가 없다면 그냥 반환을 해준다.
-> 에러는 논리적으로 로딩의 변화에 종속되기 때문에 제외햇다. - 만약현재 관찰중인 것이 있다면 전부다 커넥션을 끊어준다
- ref.current에 IntersectionObserver를 지정하고 만약 교차가 되었고 다음페이지가 있으면
현재 페이지를 다음페이지로 지정하여 page값을 dependency를 가지고 있는 customhook를 실행하도록 한다. - 노드가 있다면 해당노드를 관찰하다록 한다. (여기서는 페이지 마지막에 요소를 노드로 지정햇다)
전체 코드
export default function Home() {
const [currentPage, setCurrentPage] = useState(0);
const [diaryList, next, error, loading] = useHomePosts(currentPage);
const observer = useRef();
const lastPostElementRef = useCallback(
(node) => {
console.log('lastPostElementRef', node, loading, next, observer.current);
if (loading || next == null||node==null) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && next != null) {
console.log('ref,entries[0].isIntersecting', entries[0]);
setCurrentPage(next);
}
});
if (node) observer.current.observe(node);
},
[loading, next],
);
return (
<Box sx={{ maxWidth: 760, minWidth: 350, width: '100%' }}>
{diaryList.length > 0 ? (
<>
{diaryList.map((data) => (
<Post
key={data['diaryId']}
data={data}
style={boxStyle}
/>
))}
<Typography variant="h5" align={'center'} ref={lastPostElementRef}>
{loading && 'Loading...'}
{error != null && 'Error...'}
</Typography>
</>
) : (
<Typography
variant="h5"
align={'center'}
sx={{ paddingY: '30vh', whiteSpace: 'pre-line' }}
gutterBottom>
{'팔로잉 된 글이 없습니다.\n팔로우해보세요.'}
</Typography>
)}
</Box>
);