1. 무한 스크롤 동작 원리
👉 게시글을 한번에 여러개 가져오면?
→ 페이지 로드 속도가 엄청 나빠집니다 😢
서버에서 주는 속도도 느려지겠지만, 클라이언트에서 엄청나게 많은 컴포넌트를 그려 느려지기도 해요.
무한 스크롤은 자주 쓰는 트릭!
사실 한 화면에 담기는 게시글은 그렇게 많지 않아요. 우리가 만드는 이미지 커뮤니티의 경우에도 10개도 못들어가죠! → 화면에 담기지 않는 게시글은 불러오지 않고 있다가, 스크롤이 아래로 내려가서 '이제쯤 다음 걸 불러와야겠다' 싶을 때 (보통 마지막이나 마지막 1개 전 게시글에 스크롤이 닿았을 때!)
다음 게시글을 불러오면 사용자가 귀찮게 더보기나 다음 페이지 버튼을 누르지 않고도 쭉쭉 읽을 수 있겠죠!
(공식 문서)
https://firebase.google.com/docs/firestore?authuser=0
+ firestore에서 쿼리를 써보자
: 작성일을 기준으로 하여 데이터를 불러오기를 해보겠습니다.
const getPostFB = () => {
return function (dispatch, getState, { history }) {
const postDB = firestore.collection("post");
// 쿼리 날려서 날짜순으로 불러오기
let query = postDB.orderBy("insert_dt", "desc").limit(2);
// postDB : 어떤 collection에서 가져 올 지
// orderBy : 어떤 걸 기준으로 어떻게 정렬 할 지
// limit(갯수) : 최대 몇개를 가져올 지
query.get().then(docs => {
let post_list = [];
// redux에 넣기 전에 가져온 데이터와 redux의 initialState의 형식을 맞춰준다.
docs.forEach((doc) => {
let _post = doc.data();
// ['commenct_cnt', 'contents', ..]
let post = Object.keys(_post).reduce(
(acc, cur) => {
if (cur.indexOf("user_") !== -1) {
return {
...acc,
user_info: { ...acc.user_info, [cur]: _post[cur] },
};
}
return { ...acc, [cur]: _post[cur] };
},
{ id: doc.id, user_info: {} }
);
post_list.push(post);
});
// 데이터를 redux에 넣는다.
dispatch(setPost(post_list));
});
};
};
2. 무한 스크롤 만들기 1
👉 [How to?]
무한스크롤 컴포넌트를 만들고, 아래 4가지를 넘겨줍니다.
- 게시글 컴포넌트들 (리스트!)
- 다음 목록을 불러올 함수
- 파이어스토어에서 불러오는 중인 지, 아닌 지 판별자
- 다음 글이 있나, 없나 판별자
앗, 그러면 미리 사전에 준비할 건 뭐가 있을까요? 🙂
- 게시글을 가져올 때(파이어스토어 통신할 때), 어디까지 가져왔는 지, 다음엔 뭐부터 가져올 지 페이징 정보!
- 게시글을 가져오는 중인 지, 아닌 지 구분해야겠다!(스크롤마다 계속 다시 불러오면 안되니까!)
- 게시글을 가져오는 중이구나~ 알 수 있도록 스피너 컴포넌트를 만들자!
- paging 처리하기
1) initialState에 데이터 추가
const initialState = {
list: [],
paging: { // 페이징 정보를 담는다.
start: null, // 시작점
next: null, // 다음 게 있는 지 없는 지
size: 3, // 몇 개 가져 올건지
},
is_loading: false, // 로딩 중인지 아닌 지 판별
};
2) getPostFB에서 페이징대로 데이터를 가져오기
const getPostFB = (start = null, size=3) => {
return function (dispatch, getState, { history }) {
// state에서 페이징 정보 가져오기
let _paging = getState().post.paging;
// 시작정보가 기록되었는데 다음 가져올 데이터가 없다면? 앗, 리스트가 끝났겠네요!
// 그럼 아무것도 하지말고 return을 해야죠!
if (_paging.start && !_paging.next) {
return;
}
// 가져오기 시작~!
dispatch(loading(true));
const postDB = firestore.collection("post");
let query = postDB.orderBy("insert_dt", "desc");
// 시작점 정보가 있으면? 시작점부터 가져오도록 쿼리 수정!
if(start){
query = query.startAt(start);
}
// 사이즈보다 1개 더 크게 가져옵시다.
// 3개씩 끊어서 보여준다고 할 때, 4개를 가져올 수 있으면? 앗 다음 페이지가 있겠네하고 알 수 있으니까요.
// 만약 4개 미만이라면? 다음 페이지는 없겠죠! :)
query.limit(size+1).get().then((docs) => {
let post_list = [];
// 새롭게 페이징 정보를 만들어줘요.
// 시작점에는 새로 가져온 정보의 시작점을 넣고,
// next에는 마지막 항목을 넣습니다.
// (이 next가 다음번 리스트 호출 때 start 파라미터로 넘어올거예요.)
let paging = {
start: docs.docs[0],
next: docs.docs.length === size+1? docs.docs[docs.docs.length - 1] : null,
size: size,
};
docs.forEach((doc) => {
let _post = doc.data();
let post = Object.keys(_post).reduce(
(acc, cur) => {
if (cur.indexOf("user_") !== -1) {
return {
...acc,
user_info: { ...acc.user_info, [cur]: _post[cur] },
};
}
return { ...acc, [cur]: _post[cur] };
},
{ id: doc.id, user_info: {} }
);
post_list.push(post);
});
// 마지막 하나는 빼줍니다.
// 그래야 size대로 리스트가 추가되니까요!
// 마지막 데이터는 다음 페이지의 유무를 알려주기 위한 친구일 뿐! 리스트에 들어가지 않아요!
post_list.pop();
dispatch(setPost(post_list, paging));
});
};
};
3) 리덕스에서 기존 리스트에 데이터가 추가 되도록 리듀서 수정
[SET_POST]: (state, action) =>
produce(state, (draft) => {
draft.list.push(...action.payload.post_list);
draft.paging = action.payload.paging;
draft.is_loading = false;
}),
4) PostList 컴포넌트에 적용하기
// PostList.js
import React from "react";
import Post from "../components/Post";
import { useSelector, useDispatch } from "react-redux";
import { actionCreators as postActions } from "../redux/modules/post";
const PostList = (props) => {
const dispatch = useDispatch();
const user_info = useSelector((state) => state.user.user); // 사용자 정보
const post_list = useSelector((state) => state.post.list); // 게시물 정보
const is_loading = useSelector((state) => state.post.is_loading); // 로딩 정보
const paging = useSelector((state) => state.post.paging); // 페이징 정보
React.useEffect(() => {
if(post_list.length === 0){
dispatch(postActions.getPostFB());
}
}, []);
return (
<React.Fragment>
{post_list.map((p, idx) => {
if (user_info && p.user_info.user_id === user_info.uid) {
return <Post key={p.id} {...p} is_me />;
}
return <Post key={p.id} {...p} />;
})}
</React.Fragment>
);
};
export default PostList;
3. 무한 스크롤 만들기 2
- infinityScroll 만들기
1) 컴포넌트 만들기
import React from "react";
import _ from "lodash";
const InfinityScroll = (props) => {
const { children, callNext, is_next, loading } = props;
// 쓰로틀을 적용합시다!
const _handleScroll = _.throttle(() => {
callNext();
}
}, 300);
const handleScroll = React.useCallback(_handleScroll, [loading]);
React.useEffect(() => {
// 로딩 중이면, return!
if (loading) {
return;
}
// 다음 게 있으면 이벤트를 붙이고, 없으면 이벤트를 삭제해요!
if (is_next) {
window.addEventListener("scroll", handleScroll);
} else {
window.removeEventListener("scroll", handleScroll);
}
// 이 부분은 컴포넌트가 사라질 때 호출되는 부분입니다! (클린업이라고도 해요.)
return () => window.removeEventListener("scroll", handleScroll);
}, [is_next, loading]);
return (
<React.Fragment>
{children}
</React.Fragment>
);
};
InfinityScroll.defaultProps = {
children: null,
callNext: () => {},
is_next: false,
loading: false,
};
export default InfinityScroll;
2) 적용 해보기 (PostList.js)
- 스크롤 계산하기
👉 [브라우저 창 크기 알아보기]
페이지 끝에 닿았는 지 알려면 어떻게 할까요?
→ 스크롤할 수 있는 영역이 얼마나 남았나 계산을 해보면 됩니다!
계산을 하려면, 웹 페이지 전체 크기도 알아야하고 얼마나 스크롤했는 지도 알아야겠죠!
window.innerHeight은 가시적으로 보이는 브라우저 창 높이예요.(메뉴바 툴바 제외한 크기!) document.body.scrollHeight는 스크롤할 수 있는 높이예요.(눈에 안보이는 영역도 포함!)
document의 scrollTop은 스크롤이 얼마나 움직였나 알려주는 값이에요.
화면 크기 관련 내용 더 알아보기→ (링크)
// shared/InfinityScroll.js
const _handleScroll = _.throttle(() => {
const { innerHeight } = window;
const { scrollHeight } = document.body;
// 스크롤 계산!
const scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
if (scrollHeight - innerHeight - scrollTop < 200) {
// 로딩 중이면 다음 걸 부르면 안되겠죠!
if (loading) {
return;
}
callNext();
}
}, 300);
- 다음 목록 불러오기
// PostList.js
import React from "react";
import Post from "../components/Post";
import { useSelector, useDispatch } from "react-redux";
import { actionCreators as postActions } from "../redux/modules/post";
import InfinityScroll from "../shared/InfinityScroll";
const PostList = (props) => {
const dispatch = useDispatch();
const post_list = useSelector((state) => state.post.list);
const user_info = useSelector((state) => state.user.user);
const is_loading = useSelector((state) => state.post.is_loading);
const paging = useSelector((state) => state.post.paging);
React.useEffect(() => {
if(post_list.length === 0){
dispatch(postActions.getPostFB()); // ***
}
}, []);
return (
<React.Fragment>
<InfinityScroll
callNext={() => {
console.log("next!");
dispatch(postActions.getPostFB(paging.next));
}}
is_next={paging.next? true : false}
loading={is_loading}
>
{post_list.map((p, idx) => {
if (user_info && p.user_info.user_id === user_info.uid) {
return <Post key={p.id} {...p} is_me />;
}
return <Post key={p.id} {...p} />;
})}
</InfinityScroll>
</React.Fragment>
);
};
export default PostList;
- Spinner 붙이기
// src/elements/infinityScroll.js
import React from "react";
import _ from "lodash";
import { Spinner } from "../elements";
const InfinityScroll = (props) => {
const { children, callNext, is_next, loading } = props;
const _handleScroll = _.throttle(() => {
const { innerHeight } = window;
const { scrollHeight } = document.body;
const scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
if (scrollHeight - innerHeight - scrollTop < 200) {
if (loading) {
// 로딩 중이면 다음 리스트를 불러오지 않는다.
return;
}
callNext(); // 받아온 다음 리스트로 넘긴다.
}
}, 300);
const handleScroll = React.useCallback(_handleScroll, [loading]);
React.useEffect(() => {
if (loading) {
// 로딩 중이면 다음 리스트를 불러오지 않는다.
return;
}
if (is_next) {
window.addEventListener("scroll", handleScroll); // 스크롤 이벤트를 구독한다.
} else {
window.removeEventListener("scroll", handleScroll); // 스크롤 이벤트를 구독 해제한다.
}
return () => window.removeEventListener("scroll", handleScroll); // 클린업
}, [is_next, loading]);
return (
<React.Fragment>
{props.children}
{is_next && (<Spinner></Spinner>)}
</React.Fragment>
)
};
InfinityScroll.defaultProps = {
children: null,
callNext: () => {},
is_next: false,
loading: false,
};
export default InfinityScroll;
+ 스피너 적용하기
import React from "react";
import Spinner from "../elements/Spinner"; // ***
import _ from "lodash";
const InfinityScroll = (props) => {
{ 변경사항 없음 }
return (
<React.Fragment>
{children}
{is_next && <Spinner />} // ***
</React.Fragment>
);
};
export default InfinityScroll;
'항해 중 > 5주차 리액트 심화반' 카테고리의 다른 글
4주차 - 댓글 작성하기 (0) | 2021.12.02 |
---|---|
4주차 - 상세 페이지 연결하기 + fireStore 복합 쿼리 (0) | 2021.12.02 |
3주차 - Debounce, Throttle (0) | 2021.12.01 |
2주차 - 로그인 유지하기 / 로그아웃 구현하기 / 퀴즈 (0) | 2021.11.30 |
2주차 - 로그인하기 (0) | 2021.11.30 |
댓글