나의 코드
throttle
export const throttle = (handler, timeout = 500) => {
let invokedTime;
let timer;
return function (...args) {
if (!invokedTime) {
handler.apply(this, args);
invokedTime = Date.now();
} else {
clearTimeout(timer);
timer = window.setTimeout(() => {
if (Date.now() - invokedTime >= timeout) {
handler.apply(this, args);
invokedTime = Date.now();
}
}, Math.max(timeout - (Date.now() - invokedTime), 0));
}
};
};
- 기본값은 500밀리초, 실행 속도를 제한하여 handler 실행
- 프로세스
- 첫 번째 스크롤 이벤트는 핸들러를 즉시 트리거
- 500밀리초 내에 후속 스크롤을 할 때마다 타이머가 재설정
- 사용자가 스크롤을 중지하고 최소 500밀리초 동안 일시 중지되면
최종 스크롤 위치를 반영하여 핸들러가 다시 실행
- 예외 상황 처리
- Math.max 를 통해 제한 지연이 음수가 되지 않도록 설정
- apply 란?
- 제한된 함수를 올바른 컨텍스트 및 인수로 호출 가능
- 다양한 시나리오에 적용하도록 설정 가능
useInfinteScroll
import { useState, useEffect, useCallback } from "react";
import { initParams } from "../services/local";
import { getBackOptions } from "../api/getBackOptions";
import { throttle } from "../utils/throttle";
const useInfiniteScroll = (fetcher, { size, onSuccess, onError }) => {
const [page, setPage] = useState(1);
const [totalCounts, setTotalCounts] = useState(-1);
const [data, setData] = useState([]);
const [isFetching, setFetching] = useState(false);
const [hasNextPage, setNextPage] = useState(true);
const [backParams, setBackParams] = useState(getBackOptions(initParams()));
// 패치되는 데이터 설정 콜백
const executeFetch = useCallback(async () => {
try {
const data = await fetcher({ page, ...backParams });
setData((prev) => prev.concat(data.contents));
setTotalCounts(data.totalCounts);
setPage(data.pageNumber + 1);
setNextPage(!data.isLastPage);
setFetching(false);
onSuccess?.();
} catch (err) {
onError?.(err);
}
}, [page]);
// scroll event 발생할 시 비동기 fecth event
useEffect(() => {
const handleScroll = throttle(() => {
const { scrollTop, offsetHeight } = document.documentElement;
if (window.innerHeight + scrollTop >= offsetHeight * 0.9) {
setFetching(true);
}
});
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// fetch 해야하는데 hasNestPage 가 true 면 동작
useEffect(() => {
if (isFetching && hasNextPage) {
executeFetch();
} else if (!hasNextPage) setFetching(false);
}, [isFetching]);
// 초기화
useEffect(() => {
setPage(1);
setData([]);
setNextPage(true);
setFetching(true);
}, [backParams]);
return {
hasNextPage,
data,
totalCounts,
isFetching,
setFetching,
setBackParams,
};
};
export default useInfiniteScroll;
const scrollRef = useRef(null);
const {
data,
isFetching,
totalCounts,
hasNextPage,
setFetching,
setBackParams,
} = useInfiniteScroll(getOptionHouses, { size: 24 });
...
// reload, scroll 조건부 이벤트 발생할 때
useEffect(() => {
// if (isLastPage) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
if (scrollTop + clientHeight >= scrollHeight * 0.9) {
setFetching(true);
}
};
const throttleScrollHandler = throttle(handleScroll);
scrollRef.current.addEventListener("scroll", throttleScrollHandler);
scrollRef.current.addEventListener("beforeunload", () => {
return () =>
scrollRef.current.removeEventListener("scroll", throttleScrollHandler);
});
}, [data]);
// loading set
useEffect(() => {
if (hasNextPage) {
setLoading(isFetching);
}
}, [isFetching]);
...
/// useRef 를 Grid Layout 에 설정
return (
...
<Grid
w={"100vw"}
pl="5vw"
pr="5vw"
gridTemplateColumns={{
sm: "1fr",
md: "1fr 1fr",
lg: "repeat(3, 1fr)",
xl: "repeat(4, 1fr)",
}}
ref={scrollRef}
overflowY={"scroll"}
h="100%"
sx={scrollbarStyle}
>
{data?.map((item, idx) => {
return (
<GridItem key={idx}>
<HouseCard {...item} />
</GridItem>
);
})}
</Grid>
...
)