끝날 것 같지 않던 무한스크롤... 드디어 끝이 나는 것인가....
역방향으로 무한스크롤을 할 때 여유 있게 미리 데이터 불러오기
아.. 정말 이 부분이 제일 어려웠다.
며칠을 고민했는지... 일단 intersection observer로 해결하려고 낑낑대다가 며칠을 보냈다ㅋㅋ 하 ㅋㅋ
intersection observer로 정방향처럼 감지하기.
처음에 하려고 했던 방법은 정방향 그러니까 아래로 내렸을 때 무한 스크롤이 되듯이 intersection observer를 사용해서 역방향도 하려고 했다.
그런데... 문제가 있었다.
문제 1. 상단에 있는 intersection observer를 만나야 데이터가 불러와진다.
일단 이 문제가 제일 심각했다.
정방향을 생각해보면 하단에 도달했을 때 데이터를 불러오는데, 이게 역방향도 같은 맥락이었다.
상단에 도달해야 데이터가 불러와지는데, 한 번 제대로 불러오고 나서 사용자가 한번 아래로 살짝 내려갔다가 다시 위에 와야 이게 감지가 돼서 데이터를 불러오는 것이다. ㅋㅋㅋ
내가 유저라면 절대 사용하지 않을 것 같았다. ㅋㅋㅋ
이걸 해결하기 위해서 해본 게 역방향으로 데이터를 불러오면 스크롤을 인위적으로 아래로 내려주는 걸 해봤다. ㅋㅋㅋㅋ
일단 스크롤을 변경하는 로직이다. ㅋㅋ
먼저 데이터를 불러와서 렌더링 되는 요소의 높이를 구했다.
데이터를 불러오면 ul 안에 있는 li 요소가 렌더링 되는데, 이 요소 1개의 높이를 구하기 위해 ul에 listRef라는 useRef를 만들고, 그 요소의 children[0]
의 clientHeight
로 높이를 구했다.
그 후 scrollBoxRef의 scrollTop의 값을 itemHeigh 1개의 높이에 5(데이터를 5개씩 불러오기 때문)를 곱한 값으로 스크롤을 밑으로 내려가도록 했다.
이렇게 밑으로 내려가게 하면 유저가 다시 스크롤을 올릴게 될 것이고, 이렇게 하면 역방향 무한스크롤이 될 것 같았다.
//스크롤 변경하는 로직
const scrollPosition = (type: string) => {
const height = listRef.current?.children[0]
? listRef.current?.children[0].clientHeight
: 0
console.log('height', height)
const itemHeight = height
if (scrollBoxRef.current) {
console.log(scrollBoxRef.current.scrollTop)
const position =
type === 'top'
? scrollBoxRef.current.scrollTop + itemHeight * 5
: scrollBoxRef.current.scrollTop - itemHeight * 3
scrollBoxRef.current.scrollTop = position
window.scrollTo({
top: position,
})
}
}
예상과는 다르게 아주 매우 부자연스럽게 동작했고, 스크롤이 강제적으로 움직이다 보니 보던 데이터가 뭐였는지 헷갈리게 됐다.
결론은 아주 무쓸모 로직이라는 얘기다.
문제 2. 데이터를 여유롭게 불러올 수가 없는 점이었다.
intersection observer로 감지할 요소까지 도달해야 데이터를 불러오기 때문에 도통 데이터를 미리 불러올 수가 없었다.
rootMargin 값도 음수로 줘보고 감지할 요소의 높이도 살짝 높게 변경해 봤지만, 결과는 동일했다.
그래서 역방향은 intersection observer를 사용하지 않고 구현하기로 했다.
onScroll로 감지하기.
다음으로 생각해 낸 게 그냥 스크롤을 위로 올리는지 감지를 해서 데이터를 불러오자는 것이었다.
스크롤 감지 로직
사용자가 스크롤하며 보고 있을 때, 여유롭게 데이터를 불러오기 위해 3개의 아이템 높이값보다 스크롤이 적으면 이전 페이지의 데이터를 불러오도록 했다.
특히 중요한 부분이 3개의 아이템 높이보다 적은 스크롤값이 범위가 넓다 보니 스크롤 살짝이라도 올릴 때마다 데이터 요청을 보내는 문제가 있었는데, !isFetchingPreviousPage
로 현재 요청 중인지를 판단하는 걸 추가했더니 딱 필요한 1번의 요청만 제대로 보낼 수 있었다.
const handleScroll = () => {
const height = listRef.current?.children[0]
? listRef.current?.children[0].clientHeight
: 0
if (scrollBoxRef.current) {
if (
hasPreviousPage &&
!isFetchingPreviousPage &&
scrollBoxRef.current.scrollTop < height * 3
) {
fetchPreviousPage()
}
}
}
이중 스크롤이 생기면 불편해.
나는 해상도에 따라서 높이를 다르게 주고 싶었다. 스크롤할 영역의 높이를 고정적으로 주면 낮은 해상도에서는 스크롤이 이중으로 생겨서 불편함을 초래했다.
그래서 처음에 생각했던 방법은 무한 스크롤을 적용할 요소와 같은 화면에 있는 것들의 높이를 구해서 100vh에서 뺀 값을 높이로 주는 방법이었다.
구해서 빼야 할 부분이 헤더와 유저 정보가 있는 영역이었는데, 이걸 useRef와 getBoundingClientRect()
를 사용해 각각 높이를 구한 후 빼는 방법으로 해결하면 이 무한 스크롤을 다른 컴포넌트에서 재사용하기 너무 힘들 것 같다는 생각이 들었다.
또한, 유저의 정보가 있는 영역은 마이페이지에만 있지만, 헤더는 모든 페이지에 있다 보니 한번 구해놓은 값을 필요한 화면에서 가져가서 사용해야 편할 것 같았다.
그러려면 전역 상태로 관리해야 하는데 이건 좋지 못한 선택이라고 생각했다.
두 번째로 생각한 방법이 아예 무한 스크롤이 적용되어야 하는 영역의 top 좌표값을 getBoundingClientRect().top으로 구해서 100vh에서 빼주는 방법이었다.
이 방법은 계산도 간단하고 다른 사람이 무한스크롤을 재사용하기에도 매우 쉬운 방법이 될 것 같아서 이 방법으로 선택하게 됐다.
높이 구하기
scrollBoxRef는 무한스크롤이 적용될 요소의 useRef이다.
useEffect(() => {
//높이를 구하기 위해 사용
//100vh - top 좌표값
if (scrollBoxRef.current) {
setScrollBoxTopPosition(scrollBoxRef.current?.getBoundingClientRect().top)
}
}, [scrollBoxTopPosition])
인라인 스타일로 높이 동적으로 부여하기
tailwind는 중간에 내용이 동적으로 변하면 클래스가 추가되지 않는 불편함이 있다.
그래서 인라인 스타일을 부여해서 계산했다.
<div
ref={scrollBoxRef}
className='overflow-y-auto'
onScroll={handleScroll}
style={{
height: `calc(100vh - ${scrollBoxTopPosition}px - 30px)`,
}}
>
...생략
</div>
동적으로 높이값을 주니 intersection observer 가 제대로 동작하지 않았다.
intersection observer로 감지하는 root요소를 현재는 따로 타깃을 지정해서 ul을 감싸고 있는 부모요소인 div에서 감지하도록 만들었다.
그런데 위에서 선택한 방법을 통해 동적으로 높이를 주니 제대로 동작하지 않았다.
intersection observer는 useEffect에서 감지하도록 되어 있는데, 의존성 배열에 root를 추가하지 않아서 제대로 감지를 하지 못했던 것이다.
의존성 배열에 root를 추가하니 제대로 동작하게 됐다.
useIntersectionObserver.ts의 일부분
useEffect(() => {
if (!enabled) {
return
}
if (!target) {
return
}
//onIntersect 을 넘겨받아서 실행
const observer = new IntersectionObserver(onIntersect, {
root,
rootMargin,
threshold,
})
if (target.current) observer.observe(target.current!) //주시 대상 목록에 추가
return () => {
if (target.current) observer && observer.disconnect()
}
}, [target, rootMargin, threshold, enabled, root]) //root 추가!
최종 결과 화면
'프로젝트 > 2024' 카테고리의 다른 글
[react] 탭 메뉴 만들기 (0) | 2024.04.16 |
---|---|
최종 팀 프로젝트 Day9 (0) | 2024.04.05 |
리액트로 슬라이드 만들기 2 + typescript (0) | 2024.04.04 |
최종 팀 프로젝트 Day8 (0) | 2024.04.04 |
최종 팀 프로젝트 Day7 (1) | 2024.04.04 |