캣츄 프로젝트의 게시판 목록에서, 페이지네이션의 일종으로 무한 스크롤을 구현하기로 했다.
1. 페이지네이션 (Pagination)이란?
백엔드에 저장된 모든 정보를 한 번에 보여주지 않고, 단위갯수별로 보여주는 방법이다.
티스토리 블로그 게시글이 page별로 보여지는 것도 페이지네이션이고,
인스타그램 피드처럼 스크롤을 내릴 때 새로운 글들이 불러와지는 것도 페이지네이션이다.
페이지네이션을 제대로 구현하려면 백엔드 API의 구현도, 프론트엔드의 로직도 그에 맞게 설계되어야 한다.
페이지네이션 방법
페이지네이션에는 크게 두 방법이 있다.
1. Offset-Based Pagination (오프셋 기반)
: Offset & Limit 이용.
* 데이터를 불러오기 시작할 시작점과, 읽어들일 정보의 수를 각각 OFFSET과 LIMIT으로 표현한다!
* 위와 같은 data가 있을 때, 한 페이지 당 5개씩의 정보를 보여주고 싶다고 하자.
> 1 페이지는 OFFSET (시작점) = 0, LIMIT (읽어들일 정보의 수) = 5
> 2 페이지는 OFFSET = 5, LIMIT = 5,
> 3 페이지는 위의 이미지에서처럼, OFFSET = 10, LIMIT = 5가 될 것이다.
* 하지만 비교적 구현이 쉬운 이 방법에는 성능 문제가 있는데,
(1) 불필요한 메모리 사용 - 느림!!!
* OFFSET, LIMIT을 이용할 때 데이터베이스는 Full Table Scan (순차접근)을 한다.
* 원하는 데이터에 도달하기까지의 정보를 하나 하나 읽고, 원하는 데이터를 반환하는 것이다!
* 예를 들어 OFFSET = 50,000,000, LIMIT = 20일 때는, 앞의 50,000,000의 데이터가 필요 없음에도 불구하고,
원하는 20개의 데이터를 반환하기 위해 여전히 50,000,020개의 데이터를 읽어야한다는 뜻이다!
(2) 중첩된 내용 반환의 가능성
* 어떠한 게시판을 사용할 때, 1페이지에서 5개의 글을 읽었다고 생각해보자.
* 2페이지로 가면, OFFSET = 5, LIMIT = 5을 통해 '그 다음' 5개의 글을 읽고 싶을 것이다!
* 하지만 그 게시판이 정말 활발하여, 내가 1페이지의 글을 읽고 2페이지로 가기 전에 새롭게 5개의 글이 추가되었다면,
* 2페이지에 가도 내가 1페이지에서 읽었던 5개의 글이 그대로 보여질 것이다.
2. Cursor-Based Pagination (커서 기반)
: lastPostId & Size 이용.
* 그래서 요즘 많이 사용되는 방법이, lastPostId와 Size를 이용한 Cursor-Based Pagination이겠다!
* lastPostId, 즉 내가 현 페이지에서 읽은 포스트의 가장 마지막 글의 Id와
* 그 Id로부터 시작해 다음 몇 개의 포스트를 읽어들일 지를 알려주는 SIZE를 매개변수로,
* 다음 페이지의 글들을 불러오는 것이다.
* 이 방법을 사용하면, 중간에 게시글의 추가나 삭제가 일어나더라도, 내가 전 페이지에서 읽었던 게시글 이후의 것들을 다음 페이지에서 보는 것이 보장되고,
* 앞에서부터 원하는 데이터의 장소까지 쭉 읽어야하는 상황도 발생하지 않는다.
* 정말 설명 잘 해주신 블로그 - https://wbluke.tistory.com/18
2. 그럼 어떻게 사용자가 얼마만큼 스크롤했는 지 아나요?
* 무한 스크롤 방법으로 구현하기에, 스크롤에 대한 이해가 먼저였다.
> window.scrollY - 얼마나 내렸는 지
> document.documentElement.clientHeight - 화면 보이는 길이
> document.documentElement.scrollHeight - 총 길이
위의 세 가지를 이용하면 알 수 있다.
console.log(window.scrollY, document.documentElement.clientHeight, document.documentElement.scrollHeight);
을 통해 화면을 직접 보면 이해가 쉬운데,
와 같이,
맨 밑까지 내렸을 때 (위 이미지의 세번째 줄 경우)
window.scrollY + document.documentElement.clientHeight = document.documentElement.scrollHeight;
이 되는 것이다.
3. 오키, 그래서 React에서 어떻게 구현했나요?
* 일단 백엔드 팀에서 API를 잘 수정해주셨다!
* 리액트 코드는 제로초님 강의와 구글링을 통해 구현했다. 나의 경우 postList 페이지에 로직을 구현했고, 최초 컴포넌트 마운트 시 보여질 게시글들을 이렇게 구현했다
const PostList = () => {
const dispatch = useDispatch();
// mainPosts : 게시글 불러와 보관해 줄 중앙 저장소
// hasMorePosts : 불러올 게시글이 남았는 지 판단해주는 변수
// listPostLoading : 현재 비동기 액션을 통해 게시글을 불러오고 있는 지 저장해주는 변수
const {mainPosts, hasMorePosts, listPostLoading} = useSelector((state) => state.post);
// 한 번에 불러올 게시글 갯수
const SIZE = 5
// 맨 처음 컴포넌트가 마운트 되었을 때 실행
// 처음 다섯 개의 게시글
useEffect(() => {
// 새롭게 처음 다섯 개의 게시글 불러옴
dispatch({
type: LIST_POST_REQUEST,
data: {
size: SIZE,
lastPostId: Number.MAX_SAFE_INTEGER //맨 처음에는, 읽었던 게시글 중 마지막 것의
// 아이디 (lastPostId)를 아는 것이 불가능하다
// 따라서 입력할 수 있는 최대의 숫자를 넣어주면
// 그 숫자보다 낮은 숫자를 id로 가진 포스트 중
// 처음 5개를 불러오므로
// 이렇게 초기화할 수 있다.
}
})
}, []);
// 두 번째 파라미터로 빈 배열이 오므로 최초에 컴포넌트가 마운트 되었을 때만 실행
* 그리구 실제적 무한스크롤 부분은 다음과 같이 구현을 하였다!
useEffect(() => {
function onScroll(){
// 많이 쓰는 세 가지
// scrollY: 얼마나 내렸는 지
// clientHeight: 화면 보이는 길이
// scrollHeight: 총 길이
// 따라서 끝까지 내렸을 때
// scrollY + clientHeight=scrollHeight!!
// 내 경우 스크롤이 화면에 밑에서 10px 위까지 왔을 때 새로 게시글 호출함
if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight -10) {
if (hasMorePosts && !listPostLoading && mainPosts){
dispatch({
type: LIST_POST_REQUEST,
data: {
lastPostId: mainPosts[mainPosts.length-1].id, // 지금 보고 있는 게시글 중
// 맨 마지막 게시글 id
size: SIZE
}
});
}
}
}
window.addEventListener('scroll', onScroll);
//useEffect에서 window함수 쓸 때 중요한 건
//이렇게 해제해주는 것
//메모리 누수 방지
return() => {
window.removeEventListener('scroll', onScroll);
};
}, [mainPosts, hasMorePosts, listPostLoading]);
* hasMorePosts는 불러올 게시글이 더 있나를 판단해주는 변수인데,
* post 리듀서 쪽에서 initialState는 true로 초기화를 해주고,
case LOAD_POST_SUCCESS:
...
draft.hasMorePosts = action.data.length === 5;
...
break;
* 새로 게시글을 불러올 때마다, 내가 지정한 게시글 갯수만큼 불러와지는 지로 true/false를 판별하면 될 것이다.
EX. SIZE = 5로 설정했는데, 불러와진 게시글의 갯수가 3개라면, 5개를 불러올 수 없기에 = 게시글이 더 없기에 3개만 불러와진 것. 따라서 이 경우 hasMorePosts는 false로 설정하면 된다!
완성본! 스크롤을 내릴수록 게시글을 불러오는 요청이 새로 나가는 것을 확인할 수 있다! ㅎㅎ
+ 추가 (210521)
모바일 기기에서 확인하니, 무한 스크롤이 구현되지 않았다.
구글링 후 해결한 결과론,
1) document.documentElement.ClientHeight이 모종의 이유로 데스크톱에서와 다르게 나온다.2) 터치 기기에서는 'touchmove'라는 특별한 이벤트가 있다.
따라서 위의 코드에서
1) document.documentElement.ClientHeight -> window.innerHeight
(데스크톱에서도, 우리 앱은 화면 전체 높이를 차지하니 이렇게 바꿔도 문제가 없다)
2) window.addEventListener('touchmove', onScroll)과 window.removeEventListener('touchmove', onScroll)추가
이렇게 바꿔주니 문제가 해결되었다
댓글