diff --git a/src/apis/post/index.ts b/src/apis/post/index.ts index 0b88452d..d01a6b95 100644 --- a/src/apis/post/index.ts +++ b/src/apis/post/index.ts @@ -19,8 +19,15 @@ export const createPostApi = (data: CreatePostRequest) => newRequest.post - newRequest.get(`/post`, { params: { page, take } }); +export const getPostListApi = async ({ pageParam = 1 }) => { + const response = await newRequest.get('/post', { + params: { page: pageParam, take: 10 }, + }); + return { + posts: response.data.post, + nextPage: response.data.post.length > 0 ? pageParam + 1 : undefined, // 다음 페이지 여부 확인 + }; +}; // 유저 게시글 리스트 export const getUserPostListApi = (page: number = 1, take: number = 10, userId: number) => newRequest.get(`/post`, { params: { page, take, userId } }); diff --git a/src/pages/Home/OOTD/index.tsx b/src/pages/Home/OOTD/index.tsx index edf31e5b..c1b8da5e 100644 --- a/src/pages/Home/OOTD/index.tsx +++ b/src/pages/Home/OOTD/index.tsx @@ -1,72 +1,51 @@ -import { useState, useEffect, useRef } from 'react'; +import { useRef, useEffect } from 'react'; +import { useInfiniteQuery } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; import { getPostListApi } from '@apis/post'; -import type { PostSummary } from '@apis/post/dto'; +import Loading from '@components/Loading'; import Feed from './Feed/index'; import { OOTDContainer, FeedContainer } from './styles'; const OOTD: React.FC = () => { - const [feeds, setFeeds] = useState([]); - - const isFetchingRef = useRef(false); - const isReachedEndRef = useRef(false); - const feedPageRef = useRef(1); - - // IntersectionObserver 인스턴스를 참조하는 변수 - const observerRef = useRef(null); - // observer 콜백 함수를 트리거하는 요소를 참조하는 변수 + // 무한 스크롤을 감지할 요소 const loadMoreRef = useRef(null); - // 세션 스토리지에서 이전 스크롤 위치를 가져와 초기화 - const savedScrollPosition = sessionStorage.getItem('scrollPosition'); - const scrollPositionRef = useRef(Number(savedScrollPosition) || 0); - - // 전체 게시글(피드) 조회 API - const getPostList = async () => { - // 모든 데이터를 불러왔거나 요청 중이라면 함수 실행 중단 - if (isReachedEndRef.current || isFetchingRef.current) return; - - isFetchingRef.current = true; - - try { - const response = await getPostListApi(feedPageRef.current, 20); + // Intersection Observer 인스턴스 저장 (컴포넌트 언마운트 시 해제 위함) + const observerRef = useRef(null); - if (response.isSuccess) { - if (response.data.post.length === 0) { - isReachedEndRef.current = true; - } else { - setFeeds((prevFeeds) => [...prevFeeds, ...response.data.post]); - feedPageRef.current += 1; - } - } - } finally { - isFetchingRef.current = false; - console.log(feeds); - } - }; + // React Query를 사용한 무한 스크롤 데이터 로드 + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({ + queryKey: ['posts'], // 같은 key를 가진 쿼리는 캐시됨 + queryFn: ({ pageParam }) => getPostListApi({ pageParam }), // 페이지별 데이터 가져오는 함수 + initialPageParam: 1, // 첫 번째 페이지는 1부터 시작 + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, // 다음 페이지가 존재하면 page + 1, 없으면 undefined + }); + // 디버깅 useEffect(() => { - // 데이터의 끝에 다다르면 옵저버 해제 (더이상 피드가 없으면) - if (isReachedEndRef.current && observerRef.current && loadMoreRef.current) { - observerRef.current.unobserve(loadMoreRef.current); + console.log('Query Status:', status); + console.log('Fetched Data:', data); + console.log('Fetching Next Page:', isFetchingNextPage); + console.log('Has Next Page:', hasNextPage); + }, [status, data, isFetchingNextPage, hasNextPage]); - return; - } + // Intersection Observer를 설정하여 스크롤이 마지막 요소에 닿았을 때 fetchNextPage 호출 + useEffect(() => { + if (!loadMoreRef.current || !hasNextPage) return; // 다음 페이지가 없으면 실행 X // Intersection Observer 생성 observerRef.current = new IntersectionObserver( debounce((entries) => { - const target = entries[0]; - console.log('Intersection Observer:', target.isIntersecting); - if (target.isIntersecting && !isFetchingRef.current && !isReachedEndRef.current) { - getPostList(); + // 요소가 화면에 보이면 fetchNextPage 호출 (스크롤 트리거) + if (entries[0].isIntersecting) { + fetchNextPage(); } - }, 300), + }, 300), // 디바운싱 적용 (300ms 내 반복 호출 방지) { root: null, rootMargin: '100px', @@ -74,40 +53,27 @@ const OOTD: React.FC = () => { }, ); - // 옵저버를 마지막 요소에 연결 - if (loadMoreRef.current) { - observerRef.current.observe(loadMoreRef.current); - } - return () => { - // 컴포넌트 언마운트 시 옵저버 해제 - if (observerRef.current && loadMoreRef.current) { - observerRef.current.unobserve(loadMoreRef.current); - } - }; - }, []); - - useEffect(() => { - getPostList(); - - // 세션에 저장된 이전 스크롤 위치 복원 - window.scrollTo(0, scrollPositionRef.current); + // 옵저버를 마지막 요소(loadMoreRef)에 연결 + observerRef.current.observe(loadMoreRef.current); return () => { - // 컴포넌트 언마운트 시 현재 스크롤 위치를 세션 스토리지에 저장 - sessionStorage.setItem('scrollPosition', String(window.scrollY)); + // 컴포넌트 언마운트 시 옵저버 해제 + observerRef.current?.disconnect(); }; - }, []); + }, [hasNextPage, fetchNextPage]); return ( - {feeds.map((feed) => ( -
- -
- ))} - {/* Intersection Observer가 감지할 마지막 요소 */} + {data?.pages.flatMap((page) => + page.posts.map((feed) => ( +
+ +
+ )), + )}
+ {isFetchingNextPage && } );