1. 무한스크롤이란?
무한스크롤은 웹 페이지에서 사용자가 스크롤을 할 때 새로운 콘텐츠가 끊임없이 로드되어 나타나는 디자인 패턴을 의미한다. 페이지 전환 방식과는 달리 사용자는 페이지 이동없이 계속해서 새로운 정보에 접근할 수 있다. 주로 소셜 미디어 피드, 뉴스 웹사이트, 온라인 쇼핑몰 등에서 널리 사용되고 있다.
2. 무한스크롤의 동작방식
무한스크롤은 스크롤의 길이를 감지해, 스크롤이 하단에 도달하면 자동으로 데이터를 불러오는 방식으로 동작한다.
스크롤이 내려가는 동시에 새로운 데이터를 패칭하기 때문에 사용자는 끊임없이 콘텐츠를 접할 수 있다.
3. 무한스크롤의 이점
- 성능 최적화: 한 번에 많은 양의 데이터를 가져오지 않고, 필요한 만큼만 동적으로 불러와서 효율적인 성능을 유지할 수 있다. 이는 초기 로딩 시간을 단축하고, 사용자 경험을 향상시킨다.
- 사용자 경험 향상: 사용자가 스크롤을 내릴 때마다 추가적인 데이터가 불러와져서 사용자는 끊김없이 내용을 탐색할 수 있다. 이는 페이지를 새로고침하지 않아도 새로운 데이터를 불러오는 실시간성을 제공한다. 실제로 무한스크롤을 도입한 소셜네트워크의 경우 사용자의 머무름 시간이 이전보다 증가했다.
- 서버 부하 감소: 무한스크롤을 적용하면 사용자가 스크롤을 내릴 때마다 필요한 양의 데이터만을 서버에서 가져오므로 서버에 대한 부하를 줄일 수 있다.
- 자원 효율성: 무한스크롤을 통해 사용자가 실제로 볼 내용에 초점을 맞출 수 있으므로, 불필요한 데이터를 사전에 불러오는 것을 방지하여 자원을 효율적으로 사용할 수 있다.
4. 리액트에서 무한스크롤 구현
- 라이브러리 설치
yarn add react-query //yarn add 또는 npm install을 사용
yarn add react-intersection-observer
react-query 는 스크롤이 하단에 도착하면 다음 쿼리를 날려 페이지를 불러오는 용도로 사용하고
react-intersection-observer는 div 하단에 스크롤이 도달했는지 감지하는 용도로 사용한다.
그 밖에도 무한 스크롤을 위한 라이브러리로 react-infinite-scroll-component 가 있는데ㅡ
<InfiniteScroll
next={fetchData} //다음 데이터를 불러오는 함수
hasMore={true} //다음 페이지가 있는지
loader={<h4>Loading...</h4>} //로딩시 보여줄 화면
endMessage={ //에러시 보여줄 화면
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
{items.map((_, index) => (
<div style={style} key={index}>
div - #{index}
</div>
))}
</InfiniteScroll>
처음에는 위의 라이브러리를 사용했으나 useInfiniteQuery의 옵션과 함께 사용할 경우, 첫 호출 이후부터 중복 요청이 일어나 2번씩 호출되는 문제가 발생해 react-intersection-observer로 변경하였다. react-infinite-scroll-component 를 사용하려면 react-query 대신 무한스크롤 요청 방식을 직접 구현해야 될거같음(코드를 지워버려서 기억이안남, 스크롤 컴포넌트에서도 요청하고, 쿼리에서도 요청헤서 꼬인 문제였음)
- useInfiniteQuery 살펴보기
const {
fetchNextPage, // 다음 페이지를 호출하는 함수
fetchPreviousPage, //이전 페이지를 호출하는 함수
hasNextPage, //다음 페이지를 가지고 있는지(마지막 페이지인지 판단 t/f)
hasPreviousPage, //이전 페이지를 가지고 있는지
isFetchingNextPage, //다음 페이지를 호출 중인지 = isLoading과 같은 개념
isFetchingPreviousPage, //이전 페이지를 호출 중인지
...result
} = useInfiniteQuery({
queryKey, //고유 쿼리키
queryFn: ({ pageParam = 1 }) => fetchPage(pageParam), //페이지는 1부터 시작해서 넘겨준다
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor, //lastPage가 true면 다음 페이지를 요청
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor, //위와 동일하게 이전 페이지 요청
})
실제 사용하는 코드에 대입하면 다음과 같다. api관련 함수들은 컴포넌트 외부에서 작성하고 임포트 해오는 방식으로 사용한다.
/**알림 내역 무한스크롤 */
export const useGetInfinityAlarmList = (setAlarmList) => {
//리스트 내역을 가져오는 함수 작성
const getAlarmScroll = async ({ pageParam = 0 }) => {
const { data } = await axios.get(`/api/alarm/list`, {
params: {
page: pageParam, //현재 페이지 번호
size: 10, //몇개를 가져올 건지
},
});
return data;
};
//여기있는 isLoading은 인피니트 쿼리를 호출할때 발생하는 로딩
//isFetchingNextPage는 다음 페이지를 가져올때 발생하는 로딩
const { fetchNextPage, isLoading, hasNextPage, isFetchingNextPage } = useInfiniteQuery(["AlarmList"], getAlarmScroll, {
getNextPageParam: (lastPage) => {
//lastPage는 내가 가져온 데이터
//last 값이 true인 경우 다음 페이지가 없으니 undefined 반환
//그 외의 경우는 페이지에 +1을 해 다음 페이지 번호 요청
return lastPage.data.last ? undefined : lastPage.data.number + 1;
},
onSuccess: (res) => {
//통신이 성공한 경우 받은 리스트를 합쳐서 반환
const alarmList = res.pages.map((page) => page.data.content).flat();
setAlarmList(alarmList);
},
});
return { fetchNextPage, isLoading, hasNextPage, isFetchingNextPage };
};
리스트를 조회하는 함수를 작성한 후 쿼리 안에 인자로 넣어주고 쿼리 옵션에서 내가 사용하는 메서드를 뽑아 컴포넌트 내부에서 사용할 수 있도록 리턴해준다.
fetchNextPage : 다음 페이지를 호출하는 용도
isLoading : 컴포넌트 마운트 시점에서 로딩 처리를 위한 true/false 값
hasNextPage : 다음 페이지를 가지고 있는지에 대한 true/false 값
isFetchingNextPage : 다음 페이지를 가져오고 있는지 로딩 처리를 위한 true/false 값
/**알림 리스트 무한 스크롤 조회 */
const { isFetchingNextPage, fetchNextPage, hasNextPage, isLoading } = useGetInfinityAlarmList(setAlarmList);
setAlarmList는 쿼리 요청 성공시에 리스트를 담아오는 용도
- react-intersection-observer 살펴보기
import { useInView } from "react-intersection-observer";
/* 사용자가 바닥에 도착하면 inView가 true, 안보면 false로 자동으로 변경 */
const { ref, inView } = useInView();
ref는 늘어나는 스크롤 아래에 설정해놓는다.
그러면 div에 도달 = 스크롤이 바닥에 도달 => 다음 데이터를 요청!! 이렇게 구현할 수 있다.
useEffect(() => {
if (inView && hasNextPage) {
//inview가 true = 스크롤이 바닥에 있다
// hasNextPage가 true = 다음 스크롤이 있다
// 위의 조건이 모두 부합하는 경우, 다음 페이지를 요청한다!
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
전체 코드는 다음과 같다.
const AlarmPopup = ({ handleCloseAlarmPopup }) => {
const { t } = useTranslation(["translation"]);
const [alarmList, setAlarmList] = useState([]);
/**알림 리스트 무한 스크롤 조회 */
const { isFetchingNextPage, fetchNextPage, hasNextPage, isLoading } = useGetInfinityAlarmList(setAlarmList);
/* 사용자가 바닥에 도착하면 inView가 true, 안보면 false로 자동으로 변경 */
const { ref, inView } = useInView();
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
return (
<S.AlarmPop>
<S.AlarmPopSection>
<S.AlarmPopList>
{isLoading ? <AlarmSkeleton /> : <AlarmList alarmList={alarmList} ScrollRef={ref}/>}
{isFetchingNextPage && <LoadingSpinner />}
</S.AlarmPopList>
</S.AlarmPopSection>
</S.AlarmPop>
);
};
export default AlarmPopup;
//알람리스트 컴포넌트
return <>{renderedNotifications}<div ref={ScrollRef}></div></>;
// renderedNotifications 스크롤이 생겨 늘어나는 부분
// div 스크롤 감지용 div
- 완성된 화면
알람이 로딩중인 경우, 스켈레톤 ui 적용
알림 리스트 조회
스크롤이 하단에 도달하면 로딩바 생성
=> 다음 페이지 리스트를 가져온다
dl태그가 스크롤 부분이고
div가 ref가 설정된 부분이다.
'기술 > React.js' 카테고리의 다른 글
[문서] 리액트 공식문서 톱아보기: 기본 문법(빠른 시작 가이드) (1) | 2024.10.27 |
---|---|
react에서 vite로 env 설정해서 proxy 사용하기 (+CRA에서 proxy 설정) (0) | 2024.03.20 |
useState의 업데이트 방식에 따른 차이(함수형 업데이트) (0) | 2023.11.26 |
리액트에서 "불변성"이 중요한 이유 (0) | 2023.11.26 |
리액트(React)에서 구글 리캡차(reCAPTCHA) 연결하기 (0) | 2023.11.26 |