From 6dc8812b3cdcfeca05e68cb44a9b4819d9b9d00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EB=AF=BC=EC=9E=AC?= Date: Fri, 28 Mar 2025 22:08:23 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20react-query=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + yarn.lock | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/package.json b/package.json index 099fc075..b92f9b92 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "start:json-server": "json-server --watch db.json --port 5000" }, "dependencies": { + "@tanstack/react-query": "^5.69.3", "@types/styled-components": "^5.1.34", "axios": "^1.7.2", "dayjs": "^1.11.12", diff --git a/yarn.lock b/yarn.lock index e7af7353..7172de3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -995,6 +995,18 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== +"@tanstack/query-core@5.69.2": + version "5.69.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.69.2.tgz#65d4fa172c48aa1549661b0442df6c44281a08e9" + integrity sha512-+spKBqGMSxVMhYPMHr4L7efc4CDdb0Y8nE4UxP/FjV4V3ajP3uhBsh0T7pSuObBgYkU+nY1PRkJhKNmwlHmkUg== + +"@tanstack/react-query@^5.69.3": + version "5.69.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.69.3.tgz#30b49a99af412a518babc61fe7dfa12625c3776a" + integrity sha512-EozCj1aFM/c827GQbVnvEt2x80oBax1vd2e5s/2EDeD469AZW9BPAyRK9VVyD0I1yvO6vOI+tPKXXFcadbSXvA== + dependencies: + "@tanstack/query-core" "5.69.2" + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" From b8299f3f8c5a5ee258674bddd778b5666d8c74f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EB=AF=BC=EC=9E=AC?= Date: Fri, 28 Mar 2025 22:16:32 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20query=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=B9=84?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=20=ED=95=A8=EC=88=98=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 37 +++++++++++++++++++++---------------- src/apis/post/index.ts | 6 ++++-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 28bea8f1..f987ad94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import Home from '@pages/Home'; import Login from '@pages/Login'; @@ -32,6 +33,8 @@ import { getUserInfoApi } from '@apis/user'; import { getCurrentUserId } from '@utils/getCurrentUserId'; import Loading from '@components/Loading'; +const queryClient = new QueryClient(); + const ProtectedRoute = ({ children }: { children: JSX.Element }) => { const [isAuthenticated, setIsAuthenticated] = useState(null); @@ -98,22 +101,24 @@ const publicRoutes = [ const App: React.FC = () => { return ( - - - {/* 인증이 필요한 페이지 */} - {protectedRoutes.map(({ path, element }) => ( - {element}} /> - ))} - - {/* 인증이 필요 없는 페이지 */} - {publicRoutes.map(({ path, element }) => ( - - ))} - - {/* 없는 페이지에 대한 처리 */} - } /> - - + + + + {/* 인증이 필요한 페이지 */} + {protectedRoutes.map(({ path, element }) => ( + {element}} /> + ))} + + {/* 인증이 필요 없는 페이지 */} + {publicRoutes.map(({ path, element }) => ( + + ))} + + {/* 없는 페이지에 대한 처리 */} + } /> + + + ); }; diff --git a/src/apis/post/index.ts b/src/apis/post/index.ts index 18614464..758a668e 100644 --- a/src/apis/post/index.ts +++ b/src/apis/post/index.ts @@ -17,8 +17,10 @@ export const createPostApi = (data: CreatePostRequest) => newRequest.post - newRequest.get(`/post`, { params: { page, take } }); +export const getPostListApi = async (page: number = 1, take: number = 10) => { + const { data } = await newRequest.get(`/post`, { params: { page, take } }); + return data; +}; // 유저 게시글 리스트 export const getUserPostListApi = (page: number = 1, take: number = 10, userId: number) => newRequest.get(`/post`, { params: { page, take, userId } }); From b3bc3bf6138e6184b4fbd6f724268ba9b9651079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EB=AF=BC=EC=9E=AC?= Date: Fri, 28 Mar 2025 22:53:42 +0900 Subject: [PATCH 3/4] =?UTF-8?q?doc:=20=EC=9D=B4=ED=95=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=95=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/post/index.ts | 11 +++- src/pages/Home/OOTD/index.tsx | 112 ++++++++++++---------------------- 2 files changed, 47 insertions(+), 76 deletions(-) diff --git a/src/apis/post/index.ts b/src/apis/post/index.ts index 758a668e..2adb94fd 100644 --- a/src/apis/post/index.ts +++ b/src/apis/post/index.ts @@ -17,9 +17,14 @@ export const createPostApi = (data: CreatePostRequest) => newRequest.post { - const { data } = await newRequest.get(`/post`, { params: { page, take } }); - return data; +export const getPostListApi = async ({ pageParam = 1 }) => { + const response = await newRequest.get('/post', { + params: { page: pageParam, take: 20 }, + }); + 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) => 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 && } ); From 3ca91e58382b24905d1a18887275b8f6ebad86e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EB=AF=BC=EC=9E=AC?= Date: Fri, 28 Mar 2025 23:13:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20=ED=95=9C=20=EB=B2=88=EC=97=90=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=99=80=EC=A7=80=EB=8A=94=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=88=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/post/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apis/post/index.ts b/src/apis/post/index.ts index 2adb94fd..0d7721fe 100644 --- a/src/apis/post/index.ts +++ b/src/apis/post/index.ts @@ -19,7 +19,7 @@ export const createPostApi = (data: CreatePostRequest) => newRequest.post { const response = await newRequest.get('/post', { - params: { page: pageParam, take: 20 }, + params: { page: pageParam, take: 10 }, }); return { posts: response.data.post,