무한 스크롤 구현하려고 하니 너무 막막해서 하나씩 적어보려고 한다.
0. 무한 스크롤은 어떤 순서로 동작할까?
일단 무한 스크롤의 동작을 상상해보았다. 아래처럼 움직이면 되지않을까?
// 1. 최초 list 로딩
// 2. scroll 최하단 감지
// 3. hasNextPage 여부 체크 -> false일 때 fetch 요청하지 않도록
// 4. fetch해서 리스트 불러오기
// 5. 화면에 뿌리기 ==> react는 fetch한 데이터 set하면 된다.
1. 최초 list 로딩
const [posts, setPosts] = useState([]);
useEffect(() => {
(async () => {
api.getRecentPosts().then((list) => {
setPosts((posts) => [...posts, ...list]);
});
})();
}, []);
2. scroll 최하단 알아내기
최하단에서 5px 이하로 남아있으면 데이터를 추가로 가져와서 뿌려줬으면 좋겠다.
남은 스크롤 길이는 어떻게 알아내지?
react 무한 스크롤 검색하면 코드를 쭉 볼 수 있겠지만, 난 내가 직접 짜보고 더 좋은 답을 보고 싶었다.
그래서 scrollable height 를 구글링했고 몇가지 keyword를 찾아냈다.
- window.scrollY : 현재 스크롤 y축 위치
- document.documentElement.scrollHeight - document.documentElement.clientHeight
: 찾았다! 남은 스크롤 가능한 길이
scroll event를 우선 걸고, 최하단 길이가 5이하일 때만 동작할 수 있도록 분기처리를 해주었다.
useEffect(() => {
window.addEventListener('scroll', handleScrollEvent);
return () => {
window.removeEventListener('scroll', handleScrollEvent);
};
}, []);
const handleScrollEvent = useCallback(async () => {
const element = document.documentElement;
const scorllableHeight = element.scrollHeight - element.clientHeight;
const restScrollHeight = scorllableHeight - scrollY;
if (restScrollHeight > 5) {
return;
}
// 데이터 받아와서 뿌려주자!!!
}, []);
참고) 직접 스크롤해보면서 안에 무엇이 들어있는지 메모해보았다.
const element = document.documentElement;
// body height = element.scrollheight = clientHeight + scrollTop
// window.scrollY = element.scrollTop : 실제 유저의 스크롤 높이
// element.scrollHeight - element.clientHeight = 스크롤할 수 있는 최대 height
window.addEventListener('scroll', () => {
console.log(
'==> ',
element.clientHeight,
element.scrollTop,
element.scrollHeight,
element.clientHeight + element.scrollTop,
element.scrollHeight - element.clientHeight
);
console.log(scrollY);
});
}, []);
3. hasNextPage 여부 체크 -> false일 때 fetch 요청하지 않도록
a. hasNextPage 가 true면 scroll event 중단 (데이터 불러오지 말것)
b. 새로 불러온 list.length ===0 이면 hasPage = false 로 변경
const [pageNo, setPageNo] = useState(0);
const [posts, setPosts] = useState([]);
const [hasNextPage, setHasNextPage] = useState(true);
useEffect(() => {
(async () => {
api.getRecentPosts({ startNo: pageNo, rownum: POSTS_ROWNUM }).then((list) => {
if (list.length === 0) {
setHasNextPage(false);
return;
}
setPosts((posts) => [...posts, ...list]);
});
})();
}, []);
const handleScrollEvent = useCallback(async () => {
const element = document.documentElement;
const scorllableHeight = element.scrollHeight - element.clientHeight;
const restScrollHeight = scorllableHeight - scrollY;
if (!hasNextPage || restScrollHeight > 5) {
return;
}
// 데이터 받아와서 뿌려주자!!!
}, []);
4. fetch해서 데이터 갖고오기
1. API를 통해서 posts를 갖고오는데, paging처리를 할 수 있도록 변경했다.
2. 몇번째 줄(startNo)를 넘기게 되면서, fetch하는 부분이 pageNo가 바뀔때마다 실행되도록 dependency를 넣어주었다.
3. scroll event 시 데이터 받아올 때 pageNo를 바꿔주면 fetch가 트리거된다 ?! -> 바로 setPageNo 때려박기!
const [pageNo, setPageNo] = useState(0);
const [posts, setPosts] = useState([]);
const [hasNextPage, setHasNextPage] = useState(true);
const ROWNUM = 13;
useEffect(() => {
(async () => {
api.getRecentPosts({ startNo: pageNo, rownum: ROWNUM }).then((list) => {
if (list.length === 0) {
setHasNextPage(false);
return;
}
setPosts((posts) => [...posts, ...list]);
});
})();
}, [ pageNo ]);
const handleScrollEvent = useCallback(async () => {
const element = document.documentElement;
const scorllableHeight = element.scrollHeight - element.clientHeight;
const restScrollHeight = scorllableHeight - scrollY;
if (!hasNextPage || restScrollHeight > 5) {
return;
}
setPageNo((pageNo) => pageNo + ROWNUM + 1);
}, []);
useEffect(() => {
window.addEventListener('scroll', handleScrollEvent);
return () => {
window.removeEventListener('scroll', handleScrollEvent);
};
}, []);
5. list 뿌려주기!
이미 fetch할 때 list 값이 있으면 setPosts하도록 만들어져 있어 수정할 것이 없다! 완성!
const [pageNo, setPageNo] = useState(0);
const [posts, setPosts] = useState([]);
const [hasNextPage, setHasNextPage] = useState(true);
const ROWNUM = 13;
useEffect(() => {
(async () => {
api.getRecentPosts({ startNo: pageNo, rownum: ROWNUM }).then((list) => {
if (list.length === 0) {
setHasNextPage(false);
return;
}
setPosts((posts) => [...posts, ...list]);
});
})();
}, [ pageNo ]);
const handleScrollEvent = useCallback(async () => {
const element = document.documentElement;
const scorllableHeight = element.scrollHeight - element.clientHeight;
const restScrollHeight = scorllableHeight - scrollY;
if (!hasNextPage || restScrollHeight > 5) {
return;
}
setPageNo((pageNo) => pageNo + ROWNUM + 1);
}, []);
useEffect(() => {
window.addEventListener('scroll', handleScrollEvent);
return () => {
window.removeEventListener('scroll', handleScrollEvent);
};
}, []);
참고한 글)
- https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
- https://velog.io/@sanghyeon/React%EC%97%90%EC%84%9C-Scroll-Event-%EA%B0%90%EC%A7%80%ED%95%98%EA%B8%B0