Skip to content

Commit

Permalink
리액트 심화 과정 - 4강 무한 스크롤
Browse files Browse the repository at this point in the history
  • Loading branch information
uvula6921 committed Jul 5, 2021
1 parent 181bf74 commit 8a0eee5
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 56 deletions.
54 changes: 54 additions & 0 deletions image-community/src/elements/Spinner.js
@@ -0,0 +1,54 @@
import React from "react";
import styled from "styled-components";

const Spinner = (props) => {
const { type, size, is_dim } = props;

return (
<React.Fragment>
<SpinnerWrap type={type} is_dim={is_dim}>
<SpinnerSvg size={size} />
</SpinnerWrap>
</React.Fragment>
);
};

Spinner.defaultProps = {
type: "inline", // inline, page
is_dim: false,
size: 60,
};

const SpinnerWrap = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
${(props) =>
props.type === "page"
? `position: fixed;
height: 95vh;
top: 0;
left: 0;
padding: 0;
zIndex: 9999;`
: ``}
${(props) =>
props.is_dim
? `
background: rgba(0,0,0,0.4);
height: 100vh;
`
: ``}
`;

const SpinnerSvg = styled.div`
--size: ${(props) => props.size}px;
width: var(--size);
height: var(--size);
background-image: url(\"data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='margin: auto; background: none; display: block; shape-rendering: auto;' width='200px' height='200px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Cg transform='rotate(0 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.9166666666666666s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(30 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.8333333333333334s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(60 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.75s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(90 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.6666666666666666s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(120 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.5833333333333334s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(150 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.5s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(180 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.4166666666666667s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(210 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.3333333333333333s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(240 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.25s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(270 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.16666666666666666s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(300 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='-0.08333333333333333s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3Cg transform='rotate(330 50 50)'%3E%3Crect x='47' y='24' rx='3' ry='3.36' width='6' height='12' fill='%23222222'%3E%3Canimate attributeName='opacity' values='1;0' keyTimes='0;1' dur='1s' begin='0s' repeatCount='indefinite'%3E%3C/animate%3E%3C/rect%3E%3C/g%3E%3C/svg%3E\");
background-size: var(--size);
`;

export default Spinner;
3 changes: 2 additions & 1 deletion image-community/src/elements/index.js
Expand Up @@ -3,4 +3,5 @@ import Image from "./Image";
import Text from "./Text";
import Button from "./Button";
import Input from "./Input";
export { Grid, Image, Text, Button, Input };
import Spinner from "./Spinner";
export { Grid, Image, Text, Button, Input, Spinner };
29 changes: 20 additions & 9 deletions image-community/src/pages/PostList.js
Expand Up @@ -2,27 +2,38 @@ import React from "react";
import Post from "../components/Post";
import { useDispatch, useSelector } 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>
{/* <Post></Post> */}
{post_list.map((p, idx) => {
if (p.user_info.user_id === user_info?.uid) {
return <Post key={p.id} {...p} is_me></Post>;
} else {
return <Post key={p.id} {...p}></Post>;
}
// p에 있는 각 key, value를 한 번에 props로 넘겨줄때 {...p} 이렇게 쓰면 되네...?
})}
<InfinityScroll
callNext={() => {
dispatch(postActions.getPostFB(paging.next));
}}
is_next={paging.next ? true : false}
loading={is_loading}
>
{post_list.map((p, idx) => {
if (p.user_info.user_id === user_info?.uid) {
return <Post key={p.id} {...p} is_me></Post>;
} else {
return <Post key={p.id} {...p}></Post>;
}
// p에 있는 각 key, value를 한 번에 props로 넘겨줄때 {...p} 이렇게 쓰면 되네...?
})}
</InfinityScroll>
</React.Fragment>
);
};
Expand Down
139 changes: 93 additions & 46 deletions image-community/src/redux/modules/post.js
Expand Up @@ -22,69 +22,110 @@ const initialPost = {

const initialState = {
list: [],
paging: { start: null, next: null, size: 3 },
is_loading: false,
};

// actions
const SET_POST = "SET_POST";
const ADD_POST = "ADD_POST";
const EDIT_POST = "EDIT_POST";
const LOADING = "LOADING";

// action creators
const setPost = createAction(SET_POST, (post_list) => ({ post_list }));
const setPost = createAction(SET_POST, (post_list, paging) => ({
post_list,
paging,
}));
const addPost = createAction(ADD_POST, (post) => ({ post }));
const editPost = createAction(EDIT_POST, (post_id, post) => ({
post_id,
post,
}));
const loading = createAction(LOADING, (is_loading) => ({ is_loading }));

// middleware actions
const getPostFB = () => {
const getPostFB = (start = null, size = 3) => {
return function (dispatch, getState, { history }) {
let _paging = getState().post.paging;
if (_paging.start && !_paging.next) {
// 시작정보가 기록되었는데 다음 가져올 데이터가 없다면? 앗, 리스트가 끝났겠네요!
// 그럼 아무것도 하지말고 return을 해야죠!
return;
}
dispatch(loading(true));
const postDB = firestore.collection("image_community");
postDB.get().then((docs) => {
let post_list = [];
docs.forEach((doc) => {
let _post = doc.data();

let post = Object.keys(_post).reduce(
// reduce 쓰는법 참고!!!
(acc, cur) => {
if (cur.indexOf("user_") !== -1) {
return {
...acc,
user_info: { ...acc.user_info, [cur]: _post[cur] },
// [cur] 이렇게 써야 cur의 변수 값이 들어가짐. 그냥 cur 쓰면 문자열이 들어감...?!
// value 가져올때도 _post.cur라고 쓰면 안되고 _post[cur] 라고 써야함
// reduce만의 특징인듯?
};
}
return { ...acc, [cur]: _post[cur] };
},
{ id: doc.id, user_info: {} }
);

// let _post = {
// id: doc.id,
// ...doc.data()
// }
// let post = {
// id: doc.id,
// user_info: {
// user_name: _post.user_name,
// user_profile: _post.user_profile,
// user_id: _post.user_id,
// },
// image_url: _post.image_url,
// contents: _post.contents,
// comment_cnt: _post.comment_cnt,
// insert_dt: _post.insert_dt,
// };

post_list.push(post);
});

dispatch(setPost(post_list));
});
let query = postDB.orderBy("insert_dt", "desc");
if (start) {
query = query.startAt(start);
}

query
.limit(size + 1)
// 사이즈보다 1개 더 크게 가져옵시다.
// 3개씩 끊어서 보여준다고 할 때, 4개를 가져올 수 있으면? 앗 다음 페이지가 있겠네하고 알 수 있으니까요.
// 만약 4개 미만이라면? 다음 페이지는 없겠죠! :)
.get()
.then((docs) => {
let post_list = [];
let paging = {
// 시작점에는 새로 가져온 정보의 시작점을 넣고,
// next에는 마지막 항목을 넣습니다.
// (이 next가 다음번 리스트 호출 때 start 파라미터로 넘어올거예요.)
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(
// reduce 쓰는법 참고!!!
(acc, cur) => {
if (cur.indexOf("user_") !== -1) {
return {
...acc,
user_info: { ...acc.user_info, [cur]: _post[cur] },
// [cur] 이렇게 써야 cur의 변수 값이 들어가짐. 그냥 cur 쓰면 문자열이 들어감...?!
// value 가져올때도 _post.cur라고 쓰면 안되고 _post[cur] 라고 써야함
// reduce만의 특징인듯?
};
}
return { ...acc, [cur]: _post[cur] };
},
{ id: doc.id, user_info: {} }
);

// let _post = {
// id: doc.id,
// ...doc.data()
// }
// let post = {
// id: doc.id,
// user_info: {
// user_name: _post.user_name,
// user_profile: _post.user_profile,
// user_id: _post.user_id,
// },
// image_url: _post.image_url,
// contents: _post.contents,
// comment_cnt: _post.comment_cnt,
// insert_dt: _post.insert_dt,
// };

post_list.push(post);
});
if (docs.docs.length === size + 1) {
post_list.pop();
}

dispatch(setPost(post_list, paging));
});
};
};

Expand Down Expand Up @@ -195,7 +236,9 @@ export default handleActions(
{
[SET_POST]: (state, action) =>
produce(state, (draft) => {
draft.list = action.payload.post_list;
draft.list.push(...action.payload.post_list);
draft.paging = action.payload.paging;
draft.is_loading = false;
}),

[ADD_POST]: (state, action) =>
Expand All @@ -207,6 +250,10 @@ export default handleActions(
let idx = draft.list.findIndex((p) => p.id === action.payload.post_id);
draft.list[idx] = { ...draft.list[idx], ...action.payload.post };
}),
[LOADING]: (state, action) =>
produce(state, (draft) => {
draft.is_loading = action.payload.is_loading;
}),
},
initialState
);
Expand Down
52 changes: 52 additions & 0 deletions image-community/src/shared/InfinityScroll.js
@@ -0,0 +1,52 @@
import React from "react";
import _ from "lodash";
import { Spinner } from "../elements";

const InfinityScroll = (props) => {
const { children, callNext, is_next, loading } = props;

const _handleScroll = _.throttle(() => {
if (loading) {
return;
}

const { innerHeight } = window;
const { scrollHeight } = document.body;
const scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
if (scrollHeight - innerHeight - scrollTop < 200) {
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 size="120" />}
</React.Fragment>
);
};

export default InfinityScroll;

InfinityScroll.defaultProps = {
children: null,
callNext: () => {},
is_next: false,
loading: false,
};

0 comments on commit 8a0eee5

Please sign in to comment.