마지막 프로젝트 였던 거기어때의 백엔드 서버가 닫혀 firebase를 이용해 서비스를 할수있도록 리펙토링 중입니다.
그중 무한스크롤을 구현한 내용입니다.
기존의 무한 스크롤은 리액트 쿼리의 useInfiniteQuery훅을 사용하여 구현했었다.
import { useInView } from "react-intersection-observer";
import { useInfiniteQuery } from '@tanstack/react-query';
const [ref, inView] = useInView();
const getSearchPosts = async (searchState, pageParam) => {
const res = await postApis.searchPostAX(searchState, pageParam);
const content = res.data.data.content;
return { postList: content, isLastPage: content.length !== 10, nextPage: pageParam + 1 };
}
const result = useInfiniteQuery({
queryKey: ['postList'],
queryFn: ({ pageParam = 0 }) => getSearchPosts(searchState, pageParam),
getNextPageParam: ({ isLastPage, nextPage }) => {
if (!isLastPage) return nextPage;
},
refetchOnWindowFocus: false,
})
useEffect(() => {
// 사용자가 마지막 요소를 보고 있고, 로딩 중이 아니고 다음페이지가 있다면
if (inView && !result.isFetching && result.hasNextPage) {
result.fetchNextPage();
}
}, [inView, result.isFetching])
useEffect(() => {
if (!result.isFetching) result.refetch(searchState, 0);
}, [searchState])
react-intersection-observer라이브 러리를 사용하여 리스트의 끝에
{
result.hasNextPage && <div ref={ref} />
}
공div를 만들어 해당 div가 노출되면 다음 페이지를 받아오는 형식으로 되어있다.
이걸 그대로 사용하며 데이를받아오는 api를 파이어 베이스로만 바꾸려고 생각했다.
하지만 기존엔 검색상태가 바뀌면 api를 다시 요청하는 방식으로 상태관리를 했었지만
파이어베이스로 바꾸며 그렇게 까지 할필요가 없다고 생각했다.
이미 불러온 정보를 필터처리 하고 리액트 쿼리의 특징인 캐시 처리로 데이터는 프레쉬할 거기때문에 이방법이 덜 요청을 보낼거라 판단했다.
우선 파이어베이스의 데이터를 받아오는 모듈을 만들어야 했다.
데이터를 여러개 받아오면서 페이징 처리를 해야하기 때문에 파이어베이스 공식문서를 뒤져봤다.
역시나 방법이 있었다.
쿼리 커서로 데이터 페이지화 | Firestore | Firebase
Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 쿼리 커서로 데이터 페이지화 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세
firebase.google.com
export const getPosts = async (searchState, startAfterSnapshot) => {
const response = await getDocs(
startAfterSnapshot ?
query(
collection(db, "post"),
orderBy("writeTime", "desc"),
startAfter(startAfterSnapshot),
limit(pageLimit)
)
:
query(
collection(db, "post"),
orderBy("writeTime", "desc"),
limit(pageLimit)
)
);
const datas = response.docs.map(doc => ({ ...doc.data(), postID: doc.id }));
const lastSnapshot = response.docs[response.docs.length - 1];
return { datas, isLastPage: datas.length !== pageLimit, lastSnapshot };
}
처음에 마지막 페이지에 대한 정보가 없을것 이기에 startAfterSnapshot값이 없으면 startAfter옵션이 없는 쿼리를 요청하도록 코드를 짰다.
const result = useInfiniteQuery({
queryKey: ['postList'],
queryFn: ({ pageParam }) => getPosts(searchState, pageParam),
getNextPageParam: ({ isLastPage, lastSnapshot }) => {
if (!isLastPage) return lastSnapshot;
},
refetchOnWindowFocus: false,
})
const posts = result.data?.pages.flat(Infinity).map(posts => posts.datas).flat(Infinity);
const postsLength = posts.filter(post => post.contentType === searchState.main)
.filter(post => {
if (post.contentType === "event") {
return searchState.progress === "진행중" ? post.endPeriod >= today : post.endPeriod < today
} else if (post.contentType === "gather") {
return searchState.progress === "진행중" ? post.dateToMeet >= today : post.dateToMeet < today
} else {
return post
}
}).length;
받아온 데이터가 페이지 안에 페이지리스트들 즉 2중배열로 되어있어 배열 하나로 만들어주고 탭 상태에 따른 리스트 갯수를 파악하여 리스트가 없을때의 처리를 해주었다.
<StCardWrap>
<PageState display={postsLength === 0 ? 'flex' : 'none'} state='notFound' imgWidth='25%' height='60vh'
text='리스트가 존재하지 않습니다.' />
{(searchState.sort === "최신순" ?
posts.sort((a, b) => (new Date(b.writeTime) - new Date(a.writeTime)))
:
posts.sort((a, b) => (b.viewUsers.length - a.viewUsers.length))
)
.filter((post) =>
post.title.toUpperCase()
.includes((searchState.search || '').toUpperCase())
)
.filter((post) =>
searchState.tag.length > 0 ?
searchState.tag.includes(post.postAddress.substring(0, 2))
:
post
)
.filter((post) =>
post.contentType === "ask" ? post : searchState.progress === "진행중" ?
post.endPeriod >= today || post.dateToMeet >= today
:
post.endPeriod < today || post.dateToMeet < today
)
.filter((post) => post.contentType === searchState.main)
.map((post) => (
<Fragment key={post.postID}>
<StCardItem onClick={() => { navigate(`/${post.contentType}/${post.postID}`) }}>
<StImgBox imgUrl={post.photoURIs[0] || `${process.env.PUBLIC_URL}/No_Image_Available.jpg`} >
{post.scrapUsers.includes(localStorage.getItem('uid')) &&
<BookmarkFill style={{ margin: '4px 0 0 4px' }} />
}
</StImgBox>
<StContentBox>
<div className='titleBox'>{post.title.length > 10 ? post.title.substring(0, 9) + '...' : post.title}</div>
<div>{post.category}</div>
<div className='contentBox'>{post.content.length > 10 ? post.content.substring(0, 9) + '...' : post.content}</div>
<div className='dtateBox'>
{post.contentType === "event" && <div>{post.endPeriod}</div>}
{post.contentType === "gather" && <div>{post.dateToMeet}</div>}
{post.contentType === "ask" && <div>{post.writeTime.split(" ")[0]}</div>}
<div className='lookBox'>{
post.viewUsers.length
} <BsEye style={{ width: '16px', height: '16px', marginTop: '2px' }} /></div>
</div>
</StContentBox>
</StCardItem>
</Fragment>
))}
<PageState
display={result.isFetching ? 'flex' : 'none'}
state='notFound'
imgWidth='25%'
height=''
flexDirection='row'
text='검색중...' />
{
result.hasNextPage && <div ref={ref} />
}
</StCardWrap>
리스트의 리턴문이다.
보는바와 같이 필터처리가 좀 지저분해 보이지만 필터값이 바뀔때 마다 요청을 다시보내는 것이 아니기때문에 빠르고 비용절감 효과가 있다.
그리고 파이어베이스 에는 join같은 고급기능이 없다.
처음에는 글작성시 (댓글,스크랩,뷰횟수)는 다른 컬렉션에 저장하여 join할 생각이었다.
하지만 join이 없다는것을 알게되고 스크랩과 뷰횟수는 글 컬렉션에 저장하고 댓그만 따로 컬렉션을 만들었다.
이유는 댓글은 리스트에서 보여질 필요가 없었고 뷰횟수와 스크랩은 필요했기 때문이다.
export const insertPost = async (post) => {
return await addDoc(collection(db, "post"), post);
}
export const createComment = async (postID) => {
const initComment = { comments: [], commentUids: [], reCommentUids: [] };
return await setDoc(doc(db, "comment", postID), initComment);
}
insertPost(obj)
.then(response => {
const postID = response._key.path.segments[1];
createComment(postID)
.then(() => {
window.location.replace(`/${obj.contentType}/${postID}`);
})
.catch(error => {
console.log("createComment error", error)
})
})
.catch(error => {
console.log("fireStore insert error", error);
})
이런식으로 글작성후 해당글의 유니크값으로 댓글 컬렉션을 만드는 방식으로 했다.
생각보다 오래걸리지 않고 수정할수있었다.
기존에 나름 잘 나뉘어있어 파이어베이스를 적용시키는데 무리가 없었다.
'trouble shooting' 카테고리의 다른 글
[laravel]select where 조건으로 bcrypt암호화한 값을 넣었을때 (0) | 2023.09.11 |
---|---|
파이어베이스 댓글 단 글 리스트 불러오기 (0) | 2023.07.31 |
이미지 업로드를 위한 파이어 스토리지 사용내용 (1) | 2023.07.31 |
React AWS EC2 배포하기 (0) | 2022.11.03 |
React 다중 이미지 업로드 훅 (0) | 2022.11.02 |