GCP 과금의 압박도 있고
부트캠프 수료 후 프로젝트 손볼 시간 여유가 없어질 듯 하여
2022년 2월 마지막 주, 열심히 마지막 기능추가를 해보고 있습니다
이번 포스팅은 챌린지 목록 페이지에 무한 스크롤을 적용해본 내용입니다.
호주 정부 api 기준
부트캠프 중간 시험 중 참고하라고 알려준 페이지
React – Infinite Scroll, 무한 스크롤 구현하기
기본 구현 방식을 여기서 참고했습니다
요소 사이즈와 스크롤
scrollTop
,clientHeight
,scrollHeight
등등
요소와 스크롤 관련 위치 정보를 알 수 있는 페이지입니다
[javascript] useState 설정 메소드가 즉시 변경 사항을 반영하지 않음
useState
의 값을 변경하는 함수를 사용해도state
가 바로 바뀌지 않아서 해맬 때 도움이 되었습니다
client/src/apis/index.js
export const requestPopularChallenges = (limit, page, query) => {
let string = `${process.env.REACT_APP_API_URL}/challenges/popular?`;
string += `${limit > 0 ? 'limit=' + limit + '&' : ''}`;
string += `${page ? 'page=' + page + '&' : ''}`;
string += `${query ? 'query=' + query : ''}`;
return axios
.get(
string,
{},
{
'Content-Type': 'application/json',
}
)
.then((result) => result.data.data);
};
화면 바닥까지 스크롤 될 때마다
다음 페이지의 결과를 받아올 수 있도록
먼저 페이지 query
를 넣어줍니다
예시는 인기순 api
지만 최신순도 마찬가지로 수정
client/src/pages/challenges.js
길어서 부분 부분 잘라서 설명하겠습니다
const [sorting, setSorting] = useState('latest');
const [challenges, setChallenges] = useState([]);
const [page, setPage] = useState(1);
const [query, setQuery] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [hasNoResult, setHasNoResult] = useState(false);
const [hasNoMoreResult, setHasNoMoreResult] = useState(false);
이 페이지가 useState
로 관리하는 상태입니다
현재 받아온 챌린지가 몇 페이지인지는 page
로 표시하고,
더 이상 받아올 챌린지가 없을 때는
hasNoMoreResult
를 true
로 바꿔 저장합니다
const fetchNextData = () => {
if (!hasNoMoreResult) {
setIsLoading(true);
setPage(page => page + 1);
}
};
다음 페이지를 받아오도록 하는 함수입니다
하지만 이 안에 fetch
하는 기능은 없습니다
setPage(page => page + 1);
로 페이지만 바꿔줄 뿐인데...
useEffect(() => {
async function fetchNextPage () {
const scrollY = window.scrollY;
if (page > 1) {
if (sorting === 'latest') {
const result = await requestLatestChallenges(20, page, query);
setChallenges(challenges => [...challenges, ...result]);
if (result.length < 20) setHasNoMoreResult(true);
} else {
const result = await requestPopularChallenges(20, page, query);
setChallenges(challenges => [...challenges, ...result]);
if (result.length < 20) setHasNoMoreResult(true);
}
setIsLoading(false);
window.scrollTo(0, scrollY);
}
}
fetchNextPage()
// eslint-disable-next-line
}, [page])
다음 페이지의 챌린지 정보를 실제로 가져오는 React Hook
입니다
fetchNextData
로 page
가 바뀔 때마다 실행되어const scrollY = window.scrollY;
로 스크롤 된 높이를 저장하고,window.scrollTo(0, scrollY);
로 저장된 높이만큼 스크롤 시켜스크롤 전 & 검색 후 결과 등등
if (result.length < 20) setHasNoMoreResult(true);
<ChallengeList>
<InfiniteScroll
data={challenges}
type='challenge'
isLoading={isLoading}
fetchNextData={fetchNextData}
/>
</ChallengeList>
return
안 무한 스크롤 컴포넌트입니다
props
로는
data
data
의 type
(여기서는 없어도 되지만 나중을 위해서)isLoading
으로 중복 요청 방지fetchNextData
를 내려줍니다
client/src/components/InfiniteScroll.js
전체 코드는 이렇습니다
import React, { useEffect } from 'react';
import ChallengeCard from './ChallengeCard';
const InfiniteScroll = ({ data, type, isLoading, fetchNextData }) => {
const handleScroll = () => {
const scrollHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
if (scrollTop + clientHeight >= scrollHeight && !isLoading) {
fetchNextData();
}
};
const throttle = (func, waits) => {
let lastFunc; // timer id of last invocation
let lastRan; // time stamp of last invocation
return function (...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= waits) {
func.apply(context, args);
lastRan = Date.now();
}
}, waits - (Date.now() - lastRan));
}
};
};
useEffect(() => {
const clientWidth = document.documentElement.clientWidth
const clientHeight = document.documentElement.clientHeight;
if (clientWidth > 1024 && (60 + 16 + 40 + 70) + 954 < clientHeight) {
fetchNextData();
}
window.addEventListener('scroll', throttle(handleScroll, 500))
return () => {
window.removeEventListener('scroll', throttle(handleScroll, 500))
};
// eslint-disable-next-line
}, []);
return (
<div>
{data && type === 'challenge'
? data.map((el, index) => (
<ChallengeCard
challenge={el}
key={index}
/>
))
: null}
</div>
);
};
export default InfiniteScroll;
useEffect(() => {
const clientWidth = document.documentElement.clientWidth
const clientHeight = document.documentElement.clientHeight;
if (clientWidth > 1024 && (60 + 16 + 40 + 70) + 954 < clientHeight) {
fetchNextData();
}
window.addEventListener('scroll', throttle(handleScroll, 500))
return () => {
window.removeEventListener('scroll', throttle(handleScroll, 500))
};
// eslint-disable-next-line
}, []);
clientWidth > 1024 && (60 + 16 + 40 + 70) + 954 < clientHeight
window.addEventListener('scroll', throttle(handleScroll, 500))
useEffect
로 컴포넌트 마운트 시, scroll
이벤트에 handleScroll
함수를 달아줍니다throttle
로 0.5초에 한 번씩만 동작하도록 제한을 둡니다scroll
은 한 번에 굉장히 많이 발생하는 이벤트로return () => { window.removeEventListener('scroll', throttle(handleScroll, 500)) };
scroll
이벤트에 달았던 handleScroll
함수를 제거합니다 const handleScroll = () => {
const scrollHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
if (scrollTop + clientHeight >= scrollHeight && !isLoading) {
fetchNextData();
}
};
요소 사이즈와 스크롤
scrollTop
,clientHeight
,scrollHeight
등등
요소와 스크롤 관련 위치, 크기 정보를 알 수 있는 페이지입니다
scrollHeight
와 scrollTop
, clientHeight
를 비교하여 화면 하단까지 스크롤 되었는지 판단합니다!isLoading
으로 요청 중이 아니라고 정해졌다면 fetchNextData()
로 다음 페이지 정보를 요청합니다 return (
<div>
{data && type === 'challenge'
? data.map((el, index) => (
<ChallengeCard
challenge={el}
key={index}
/>
))
: null}
</div>
);
};
props
로 전달받은 data
를 형식에 맞게 표현합니다뿌듯
서버와 배포 상황에 맞게 수정한 후
업로드하고 잘 적용 되었는지 확인