결과적으로
useSWRInfinite()
와throttle
을 이용해서 구현했습니다.
velog
의 메인 페이지를 보면 일정 구간 이하로 스크롤을 내릴 때마다 게시글을 추가로 가져오게 됩니다.
해당 기능을 useSWRInfinite()
, throttle
, 스크롤 이벤트를 이용해서 구현했습니다.
이전에 댓글 더 불러오기를 구현할 때 설명과 예시를 작성한 적이 있기 때문에 이번 포스팅에서는 실제 적용 코드만 첨부하겠습니다.
// /api/posts?page=[page]&offset=[offset]&kinds=[kinds] 형태로 요청
const { data: responsePosts, setSize } = useSWRInfinite<ApiResponseOfPosts>(
(pageIndex, previousPageData) => {
if (previousPageData && previousPageData.posts.length !== offset) {
setHasMorePost(false);
return null;
}
if (previousPageData && !previousPageData.posts.length) {
setHasMorePost(false);
return null;
}
return `/api/posts?page=${pageIndex}&offset=${offset}&kinds=popular`;
}
);
현재 코드에서 사용한 방식은 throttle
방식입니다.
그 이유는 debounce
를 적용하면 계속 스크롤을 할 경우 요청이 계속 지연되는 문제가 발생하기 때문입니다.
throttle
이란 한번 요청하면 일정 시간 동안 해당 요청을 무시하는 것을 말합니다.
아래 코드에서는setTimeOut()
을 이용해 첫 요청 이후 400ms 이후에 새로운 요청을 받도록 구현했습니다.
import { useCallback, useEffect, useState } from "react";
type Props = {
condition: boolean;
// useSWRInfinite()가 반환하는 setSize()
setSize: (size: number | ((_size: number) => number)) => any;
};
const useInfiniteScroll = ({ condition, setSize }: Props) => {
const [throttle, setThrottle] = useState(false);
// 2022/05/06 - 인피니티 스크롤링 함수 - by 1-blue
const infiniteScrollEvent = useCallback(() => {
if (!condition) return;
if (
window.scrollY + document.documentElement.clientHeight >=
document.documentElement.scrollHeight - 400
) {
if (throttle) return;
setThrottle(true);
setTimeout(() => {
setThrottle(false);
setSize((prev) => prev + 1);
}, 400);
}
}, [condition, setSize, throttle, setThrottle]);
// 2022/05/06 - 무한 스크롤링 이벤트 등록/해제 - by 1-blue
useEffect(() => {
window.addEventListener("scroll", infiniteScrollEvent);
return () => window.removeEventListener("scroll", infiniteScrollEvent);
}, [infiniteScrollEvent]);
};
export default useInfiniteScroll;
debounce
란 연속적으로 들어온 요청 중에 가장 마지막 요청만 실행하는 것을 의미합니다.
아래 코드는setTimeout()
을 이용해서 400ms 이내에 연속적으로 요청이 들어오면clearTimeout()
을 이용해서 타이머를 해제하고 다시 등록하는 방식으로 동작합니다.
import { useCallback, useEffect, useState, useRef } from "react";
type Props = {
condition: boolean;
setSize: (size: number | ((_size: number) => number)) => any;
};
// 2022/05/06 - useSWRInfinite() + 무한 스크롤링을 적용하는 훅 - by 1-blue
const useInfiniteScroll = ({ condition, setSize }: Props) => {
// 2022/05/06 - 타이머 아이디 - by 1-blue
const timerId = useRef<any>(null)
// 2022/05/06 - 인피니티 스크롤링 함수 - by 1-blue
const infiniteScrollEvent = useCallback(() => {
if (!condition) return;
if (
window.scrollY + document.documentElement.clientHeight >=
document.documentElement.scrollHeight - 400
) {
if (timerId.current) clearTimeout(timerId.current);
timerId.current = setTimeout(() => setSize((prev) => prev + 1), 400);
}
}, [condition, setSize, throttle, setThrottle]);
// 2022/05/06 - 무한 스크롤링 이벤트 등록/해제 - by 1-blue
useEffect(() => {
window.addEventListener("scroll", infiniteScrollEvent);
return () => window.removeEventListener("scroll", infiniteScrollEvent);
}, [infiniteScrollEvent]);
};
export default useInfiniteScroll;
useSWRInfinite()
로 인해 이차원배열 형태로 데이터가 들어온다.grid
)// 2차원 배열이므로 Array.prototype.map()을 두 번 사용해서 배치
// 발생한 문제: <ul> 집합끼리만 grid가 적용돼서 중간에 게시글이 빈 부분이 생김
{responsePosts?.map(({ posts }, index) => (
<ul
key={index}
className="grid gird-col-1 gap-x-8 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
>
<>
{posts.map((post, i) => (
<Post
key={post.id}
post={post}
photoSize="w-full h-[300px]"
$priority={i < 4}
/>
))}
</>
</ul>
))}
// <ul>안에 <Post />를 렌더링해서 배치는 정상적으로 작동함
// 하지만 responsePosts?.map()에서 key를 사용하는 부분이 없기 때문에 경고가 발생함
<ul className="grid gird-col-1 gap-x-8 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{responsePosts?.map(({ posts }) => (
<>
{posts.map((post, i) => (
<Post
key={post.id}
post={post}
photoSize="w-full h-[300px]"
$priority={i < 4}
/>
))}
</>
))}
</ul>
// <Post />만 넣는 배열을 따로 만들어서 렌더링함
// 배치도 정상적이고, key에 대한 경고도 사라짐
const [list, setList] = useState<any>([]);
useEffect(() => {
setList(
responsePosts?.map(({ posts }) =>
posts.map((post, i) => (
<Post
key={post.id}
post={post}
photoSize="w-full h-[300px]"
$priority={i < 4}
/>
))
)
);
}, [responsePosts]);
// jsx
<ul className="grid gird-col-1 gap-x-8 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{list}
</ul>
스크롤 이벤트는 초당 수십번에서 수백 번까지 발동해서 그런지 throttle
이나 denouncedebounce
를 적용해도 가끔 무시하고 여러 번 요청되는 경우가 발생합니다.
이 문제는 제가 코드를 잘못 짜서 그런 건지 스크롤 이벤트가 너무 단기간에 많이 발동해서인지 정확한 원인은 파악하지 못했고 어떻게 해결할지도 찾지 못해서 일단 기록하고 넘어가겠습니다.