모바일에서 페이지네이션 방식을 사용하려면 원하는 페이지로 이동을 하기 위해 숫자 버튼을 일일이 손으로 눌러야 하기 때문에 매우 불편
이 때 사용할 수 있는게 무한 스크롤
사용자가 스크롤링 하다가 미리 로드된 콘텐츠를 다 확인하면 다음 목록을 또 로드해서 별도의 인털게션 없이 목록을 계속 불러오는 방식
Element.scrollHeight
: 엘리먼트의 총 높이를 나타내며 바깥으로 넘쳐서 보이지 않는 콘텐츠도 포함
Element.clientHeight
: 엘리먼트의 내부 높이 (padding 포함, scroll bar 높이, margin, border 미포함)
Element.offsetHeight
: 엘리먼트의 내부 높이 (padding 포함, scroll bar 높이, margin, border 포함)
Element.scrollTop
: 스크롤 바의 Top 부분이 화면에 내려온 위치
즉,
scrollHeight - clientHeight - scrollTop
이 미리 정해놓은offset
미만 일 때 스크롤이 최하단에 왔다고 판단해서 다음 페이지를 가져오고 기존 항목들에 덧붙여(append
) 주면 됩니다.
스크롤을 움직일 때마다 이벤트 발생 -> 성능 문제 야기
이를 해결하기 위해 보통 이벤트에 쓰로틀링(throttling)을 적용하여 이벤트 제한
주로 DOM 이벤트를 기반으로 실행하는 자바스크립트를 성능상의 이유로 이벤트를 제한할 때 debounce와 throttling 을 적용
: 이벤트를 그룹핑해서 특정 시간이 지난 후 하나의 이벤트만 발생하도록 하는 기술. 연달아서 호출되는 함수들 중 마지막 함수만 호출하도록 하는 것
: 이벤트를 일정한 주기마다 발생하는 기술. 마지막 함수가 호출된 후 일정 시간이 지나기 전엔 다시 호출되지 않도록 하는 방식
npx create-react-app modal-playground --template typescript
npm i axios throttle-debounce
npm i @types/throttle-debounce -D
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { throttle } from "throttle-debounce";
interface Airline {
id: number;
name: string;
country: string;
logo: string;
slogan: string;
head_quaters: string;
website: string;
extablished: string;
}
interface Passenger {
_id: string;
name: string;
trips: number;
airline: Airline;
__v: number;
}
function App() {
const listRef = useRef<HTMLUListElement>(null);
const currentPageRef = useRef<number>(0);
const [passengers, setPassengers] = useState<Array<Passenger>>([]);
const [isLast, setIsLast] = useState<boolean>(false);
const [isScrollBottom, setIsScrollBottom] = useState<boolean>(false);
const getPassengers = async (init?: boolean) => {
const params = { page: currentPageRef, size: 30 };
try {
const response = await axios.get("https://api.instantwebtools.net/v1/passenger", { params });
const passengers = response.data.data;
const isLast = response.data.totalPages === currentPageRef.current;
init ? setPassengers(passengers) : setPassengers((prev) => [...prev, ...passengers]);
setIsLast(isLast);
} catch (e) {
console.error(e);
}
};
const handleScroll = throttle(1000, () => {
if (listRef.current) {
const { scrollHeight, offsetHeight, scrollTop } = listRef.current;
const offset = 50;
console.log("trigger");
console.log(scrollTop, scrollHeight, offsetHeight);
setIsScrollBottom(scrollHeight - offsetHeight - scrollTop < offset);
}
});
useEffect(() => {
if (isScrollBottom) {
currentPageRef.current += 1;
!isLast && getPassengers();
}
}, [isScrollBottom, isLast]);
useEffect(() => {
getPassengers(true);
}, []);
return (
<div>
<ul ref={listRef} className="list" onScroll={handleScroll}>
{passengers.map((passenger) => (
<li className="item" key={passenger._id}>
{passenger.name}
</li>
))}
</ul>
</div>
);
}
export default App;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
.list {
overflow: hidden scroll;
list-style: none;
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
.item {
font-size: 24px;
}