|
1 | | -import { useState, useEffect, useRef } from 'react'; |
| 1 | +import { useRef, useEffect } from 'react'; |
2 | 2 |
|
| 3 | +import { useInfiniteQuery } from '@tanstack/react-query'; |
3 | 4 | import debounce from 'lodash/debounce'; |
4 | 5 |
|
5 | 6 | import { getPostListApi } from '@apis/post'; |
6 | 7 |
|
7 | | -import type { PostSummary } from '@apis/post/dto'; |
| 8 | +import Loading from '@components/Loading'; |
8 | 9 |
|
9 | 10 | import Feed from './Feed/index'; |
10 | 11 |
|
11 | 12 | import { OOTDContainer, FeedContainer } from './styles'; |
12 | 13 |
|
13 | 14 | const OOTD: React.FC = () => { |
14 | | - const [feeds, setFeeds] = useState<PostSummary[]>([]); |
15 | | - |
16 | | - const isFetchingRef = useRef(false); |
17 | | - const isReachedEndRef = useRef(false); |
18 | | - const feedPageRef = useRef(1); |
19 | | - |
20 | | - // IntersectionObserver 인스턴스를 참조하는 변수 |
21 | | - const observerRef = useRef<IntersectionObserver | null>(null); |
22 | | - // observer 콜백 함수를 트리거하는 요소를 참조하는 변수 |
| 15 | + // 무한 스크롤을 감지할 요소 |
23 | 16 | const loadMoreRef = useRef<HTMLDivElement | null>(null); |
24 | 17 |
|
25 | | - // 세션 스토리지에서 이전 스크롤 위치를 가져와 초기화 |
26 | | - const savedScrollPosition = sessionStorage.getItem('scrollPosition'); |
27 | | - const scrollPositionRef = useRef(Number(savedScrollPosition) || 0); |
28 | | - |
29 | | - // 전체 게시글(피드) 조회 API |
30 | | - const getPostList = async () => { |
31 | | - // 모든 데이터를 불러왔거나 요청 중이라면 함수 실행 중단 |
32 | | - if (isReachedEndRef.current || isFetchingRef.current) return; |
33 | | - |
34 | | - isFetchingRef.current = true; |
35 | | - |
36 | | - try { |
37 | | - const response = await getPostListApi(feedPageRef.current, 20); |
| 18 | + // Intersection Observer 인스턴스 저장 (컴포넌트 언마운트 시 해제 위함) |
| 19 | + const observerRef = useRef<IntersectionObserver | null>(null); |
38 | 20 |
|
39 | | - if (response.isSuccess) { |
40 | | - if (response.data.post.length === 0) { |
41 | | - isReachedEndRef.current = true; |
42 | | - } else { |
43 | | - setFeeds((prevFeeds) => [...prevFeeds, ...response.data.post]); |
44 | | - feedPageRef.current += 1; |
45 | | - } |
46 | | - } |
47 | | - } finally { |
48 | | - isFetchingRef.current = false; |
49 | | - console.log(feeds); |
50 | | - } |
51 | | - }; |
| 21 | + // React Query를 사용한 무한 스크롤 데이터 로드 |
| 22 | + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({ |
| 23 | + queryKey: ['posts'], // 같은 key를 가진 쿼리는 캐시됨 |
| 24 | + queryFn: ({ pageParam }) => getPostListApi({ pageParam }), // 페이지별 데이터 가져오는 함수 |
| 25 | + initialPageParam: 1, // 첫 번째 페이지는 1부터 시작 |
| 26 | + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, // 다음 페이지가 존재하면 page + 1, 없으면 undefined |
| 27 | + }); |
52 | 28 |
|
| 29 | + // 디버깅 |
53 | 30 | useEffect(() => { |
54 | | - // 데이터의 끝에 다다르면 옵저버 해제 (더이상 피드가 없으면) |
55 | | - if (isReachedEndRef.current && observerRef.current && loadMoreRef.current) { |
56 | | - observerRef.current.unobserve(loadMoreRef.current); |
| 31 | + console.log('Query Status:', status); |
| 32 | + console.log('Fetched Data:', data); |
| 33 | + console.log('Fetching Next Page:', isFetchingNextPage); |
| 34 | + console.log('Has Next Page:', hasNextPage); |
| 35 | + }, [status, data, isFetchingNextPage, hasNextPage]); |
57 | 36 |
|
58 | | - return; |
59 | | - } |
| 37 | + // Intersection Observer를 설정하여 스크롤이 마지막 요소에 닿았을 때 fetchNextPage 호출 |
| 38 | + useEffect(() => { |
| 39 | + if (!loadMoreRef.current || !hasNextPage) return; // 다음 페이지가 없으면 실행 X |
60 | 40 |
|
61 | 41 | // Intersection Observer 생성 |
62 | 42 | observerRef.current = new IntersectionObserver( |
63 | 43 | debounce((entries) => { |
64 | | - const target = entries[0]; |
65 | | - console.log('Intersection Observer:', target.isIntersecting); |
66 | | - if (target.isIntersecting && !isFetchingRef.current && !isReachedEndRef.current) { |
67 | | - getPostList(); |
| 44 | + // 요소가 화면에 보이면 fetchNextPage 호출 (스크롤 트리거) |
| 45 | + if (entries[0].isIntersecting) { |
| 46 | + fetchNextPage(); |
68 | 47 | } |
69 | | - }, 300), |
| 48 | + }, 300), // 디바운싱 적용 (300ms 내 반복 호출 방지) |
70 | 49 | { |
71 | 50 | root: null, |
72 | 51 | rootMargin: '100px', |
73 | 52 | threshold: 0, |
74 | 53 | }, |
75 | 54 | ); |
76 | 55 |
|
77 | | - // 옵저버를 마지막 요소에 연결 |
78 | | - if (loadMoreRef.current) { |
79 | | - observerRef.current.observe(loadMoreRef.current); |
80 | | - } |
81 | | - return () => { |
82 | | - // 컴포넌트 언마운트 시 옵저버 해제 |
83 | | - if (observerRef.current && loadMoreRef.current) { |
84 | | - observerRef.current.unobserve(loadMoreRef.current); |
85 | | - } |
86 | | - }; |
87 | | - }, []); |
88 | | - |
89 | | - useEffect(() => { |
90 | | - getPostList(); |
91 | | - |
92 | | - // 세션에 저장된 이전 스크롤 위치 복원 |
93 | | - window.scrollTo(0, scrollPositionRef.current); |
| 56 | + // 옵저버를 마지막 요소(loadMoreRef)에 연결 |
| 57 | + observerRef.current.observe(loadMoreRef.current); |
94 | 58 |
|
95 | 59 | return () => { |
96 | | - // 컴포넌트 언마운트 시 현재 스크롤 위치를 세션 스토리지에 저장 |
97 | | - sessionStorage.setItem('scrollPosition', String(window.scrollY)); |
| 60 | + // 컴포넌트 언마운트 시 옵저버 해제 |
| 61 | + observerRef.current?.disconnect(); |
98 | 62 | }; |
99 | | - }, []); |
| 63 | + }, [hasNextPage, fetchNextPage]); |
100 | 64 |
|
101 | 65 | return ( |
102 | 66 | <OOTDContainer> |
103 | 67 | <FeedContainer> |
104 | | - {feeds.map((feed) => ( |
105 | | - <div key={feed.id}> |
106 | | - <Feed feed={feed} /> |
107 | | - </div> |
108 | | - ))} |
109 | | - {/* Intersection Observer가 감지할 마지막 요소 */} |
| 68 | + {data?.pages.flatMap((page) => |
| 69 | + page.posts.map((feed) => ( |
| 70 | + <div key={feed.id}> |
| 71 | + <Feed feed={feed} /> |
| 72 | + </div> |
| 73 | + )), |
| 74 | + )} |
110 | 75 | <div ref={loadMoreRef} /> |
| 76 | + {isFetchingNextPage && <Loading />} |
111 | 77 | </FeedContainer> |
112 | 78 | </OOTDContainer> |
113 | 79 | ); |
|
0 commit comments