원티드 프리온보딩 첫 번째 과제의 주요 기능인 Infinity Scroll에 관한 글입니다.
redux와
redux-saga
를 이용해 전역 상태 관리, 비동기 네트워크 통신을 했습니다.
앞으로 나오는 이벤트들에 의해 page
가 변하게 되고, 해당 page
에 맞는 데이터들을 가져옵니다.
useEffect(() => {
dispatch(fetchCommentsRequest(page));
}, [dispatch, page]);
스크롤 이벤트
와 height
를 이용한 방법입니다.
useEffect(() => {
function onScroll() {
// window.scrollY : 내가 보고 있는 화면의 최상단 지점의 높이. 즉 스크롤을 통해 내린 height.
// document.documentElement.clientHeight : 내 모니터 화면의 height.
// document.documentElement.scrollHeight : 페이지의 총 높이 height.
if (
window.scrollY + document.documentElement.clientHeight >
document.documentElement.scrollHeight - 300
) {
// 조건 만족 시 다음 페이지 Set
const nextPage = page + 1;
setPage(nextPage);
}
}
window.addEventListener("scroll", onScroll);
return () => {
// 뒷정리 함수를 이용해 스크롤이 끝나면 스크롤 이벤트 제거
window.removeEventListener("scroll", onScroll);
};
}, [page]);
height
를 이용하기 때문에 무슨 의미인지 한눈에 알아볼 수 있었습니다.throttle
을 구현해야 하는 불편함이 있습니다.throttle
을 이용해 해결해야 하는 불편함이 있습니다.스크롤 이벤트가 height를 이용한다면 Intersection Observer
은 구역을 이용하는 느낌입니다. 브라우저의 뷰포트와 설정한 요소에 포착된다면 발생합니다.
const [target, setTarget] = useState(null);
useEffect(() => {
let observer;
const _onIntersect = ([entry]) => {
// 포착 된 경우
if (entry.isIntersecting) {
const nextpage = page + 1;
setPage(nextpage);
}
};
if (target) {
// observer 생성, 가시성 범위 설정
observer = new IntersectionObserver(_onIntersect, { threshold: 1 });
// 설정한 요소 관찰
observer.observe(target);
}
// observer 삭제
return () => observer && observer.disconnect();
}, [page, target, fetchLoading]);
return (
<div>
<CardList />
<div ref={setTarget}>loading</div>
</div>
);
react-intersection-observer 패키지를 이용했고 최종적으로 선택한 방법입니다. useInView hook을 이용한 간단한 사용법과 그로 인한 짧은 코드 길이가 특징입니다.
import { useInView } from "react-intersection-observer";
...
// ref 설정과 관찰 여부 변수
const [ref, inView] = useInView();
useEffect(() => {
// 관찰
if (inView) {
dispatch(fetchCommentsRequest(page));
const nextPage = page + 1;
setPage(nextPage);
}
}, [inView, page, dispatch]);
return (
<>
<CommentList />
// 관찰 요소
<div ref={ref} />
</>
);
무슨 소리냐면.. 요소를 관찰하기 위해 만든 dom이 아직 데이터를 못 받아와 dom을 못 그리고 있을 때 뷰포트에 관찰 당해버리면 어떻게 되냐는 의미 입니다. (?)
당연히 감지되어 dispatch가 발생합니다. 하지만 왜인지 dispatch가 무한히 발생해 터져버렸습니다.
fetchLoading
변수를 이용했습니다. fetchLoading
이 false
일 때 즉, REQUEST
가 끝난 SUCCESS
, FAILURE
상태일 때만 호출되도록 구현했습니다.
// comment reducer
const comment = (state = initialStete, action) => {
switch (action.type) {
// 네트워크 요청 때 fetchLoading을 true로 설정합니다.
// 요청이 SUCCESS, FAILURE 됐을 때 false로 변합니다.
case FETCH_COMMENTS_REQUEST:
return {
...state,
fetchDone: false,
fetchError: null,
fetchLoading: true,
};
...
}
};
// 컴포넌트
useEffect(() => {
// fetchLoading가 flase일 때, 즉 request 상태가 아닐때만 발생합니다.
if (inView && !fetchLoading) {
dispatch(fetchCommentsRequest(page));
const nextPage = page + 1;
setPage(nextPage);
}
}, [inView, page, fetchLoading, hasMoreComments, dispatch]);
만약 끝까지 스크롤 해 더 이상 불러올 데이터가 없다면 어떻게 될지 고려해봤습니다. postman으로 확인해본 결과 총 500개의 댓글이 있는 것을 알 수 있었고 50번 스크롤 해서 마지막까지 스크롤 해봤습니다. ( limit을 바꾸면 한 번에 확인 가능합니다.. )
더 이상 로드할 데이터가 없어 같은 뷰포트에 머물러 있기 때문에 수많은 이벤트가 발생하고 있었습니다.
지금이 마지막 페이지라는 것을 알려줄 hasMoreComments
변수로 해결했습니다. fetch 요청이 성공했을 때, 받아온 데이터들의 길이를 이용해, 받아온 데이터의 개수가 0개라면 더 이상 받아올 데이터가 없음을 이용했습니다.
hasMoreComments를
이용해 dispatch
발생 여부를 결정했습니다.
// comment reducer
const comment = (state = initialStete, action) => {
switch (action.type) {
case FETCH_COMMENTS_SUCCESS:
return {
...state,
// 받아올 데이터가 남아 있나요?
hasMoreComments: action.payload.length !== 0,
comments: state.comments.concat(action.payload),
fetchDone: true,
fetchError: null,
fetchLoading: false,
};
}
};
useEffect(() => {
// 관찰 되어야 하고, 로딩도 끝나야하고, 더 받아올 데이터도 있어야지 다음 페이지 로딩
if (inView && !fetchLoading && hasMoreComments) {
dispatch(fetchCommentsRequest(page));
const nextPage = page + 1;
setPage(nextPage);
}
}, [inView, page, fetchLoading, hasMoreComments, dispatch]);
return (
<>
<CommentList />
<div ref={hasMoreComments && !fetchLoading ? ref : undefined} />
</>
);