티스토리 뷰
🌴 React Query (현 TanStack Query)
서버의 데이터 가져오기/업데이트, 캐싱, 에러 처리 등을 쉽게 할 수 있도록 돕는 라이브러리
서버에서 가져온 값을 담는 객체, 에러가 발생했을 때 에러 정보를 담는 객체, 데이터 가져오기/업데이트 중임을 나타내는 등 각종 유틸 기능을 제공한다.
동일한 요청을 동시에 여러번 해도 한번만 요청을 보내 최적화하기 때문에 비동기 작업을 좀 더 효율적이고 간단하게 처리할 수 있게 한다.
cursor에 따라 postId 변경하기
http.get("/api/postRecommends", ({ request }) => {
const url = new URL(request.url);
// cursor의 기본값은 0
const cursor = parseInt(url.searchParams.get("cursor") as string) || 0;
return HttpResponse.json([
{
postId: cursor + 1, // cursor에 따라 id가 변경됨
User: User[0],
content: `${cursor + 1} Z.com is so marvelous. I'm gonna buy that.`,
Images: [{ imageId: 1, link: faker.image.urlLoremFlickr() }],
createdAt: generateDate(),
},
{
postId: cursor + 2,
User: User[0],
content: `${cursor + 2} Z.com is so marvelous. I'm gonna buy that.`,
Images: [
{ imageId: 1, link: faker.image.urlLoremFlickr() },
{ imageId: 2, link: faker.image.urlLoremFlickr() },
],
createdAt: generateDate(),
},
{
postId: cursor + 3,
User: User[0],
content: `${cursor + 3} Z.com is so marvelous. I'm gonna buy that.`,
Images: [],
createdAt: generateDate(),
},
{
postId: cursor + 4,
User: User[0],
content: `${cursor + 4} Z.com is so marvelous. I'm gonna buy that.`,
Images: [
{ imageId: 1, link: faker.image.urlLoremFlickr() },
{ imageId: 2, link: faker.image.urlLoremFlickr() },
{ imageId: 3, link: faker.image.urlLoremFlickr() },
{ imageId: 4, link: faker.image.urlLoremFlickr() },
],
createdAt: generateDate(),
},
{
postId: cursor + 5,
User: User[0],
content: `${cursor + 5} Z.com is so marvelous. I'm gonna buy that.`,
Images: [
{ imageId: 1, link: faker.image.urlLoremFlickr() },
{ imageId: 2, link: faker.image.urlLoremFlickr() },
{ imageId: 3, link: faker.image.urlLoremFlickr() },
],
createdAt: generateDate(),
},
]);
}),
데이터는 5개의 더미 데이터를 사용하고 있다.
request의 url에서 cursor를 가져와 데이터의 postId로 사용한다.
postId=0이면 postId가 1, 2, 3, 4, 5인 데이터 5개를 전달
postId=5이면 postId가 6, 7, 8, 9, 10인 데이터 5개를 전달
postid=10이면 postId가 11, 12, 13, 14, 15인 데이터 5개를 전달
이 동작을 반복해 데이터를 계속 불러온다.
데이터 요청하기
- 서버 컴포넌트
무한 스크롤을 적용할 페이지에서 기존에 사용하던 queryClient.prefetchQuery를 queryClient.prefetchInfiniteQuery로 변경한다.
const queryClient = new QueryClient();
await queryClient.prefetchInfiniteQuery({
queryKey: ["posts", "recommends"],
queryFn: getPostRecommends,
initialPageParam: 0,
});
initialPageParam은 cursor의 초기값으로 0을 전달한다.
- 클라이언트 컴포넌트
클라이언트 컴포넌트에서는 useQuery대신 useInfiniteQuery를 사용한다.
const { data } = useInfiniteQuery<
IPost[],
Object,
InfiniteData<IPost[]>,
[_1: string, _2: string],
number // initialPageParam 타입
>({
queryKey: ["posts", "recommends"],
queryFn: getPostRecommends,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.at(-1)?.postId,
staleTime: 60 * 1000,
});
initialPageParam을 전달하고 getNextPageParam를 추가한다.
lastPage는 가장 최근에 불러온 5개의 데이터가 들어있어 lastPage의 가장 마지막 데이터의 postId를 cursor로 전달한다.
+ getPostRecommend 함수에서 cursor 보내기
getPostRecommend 함수에서 pageParam을 받아 그대로 전달한다.
type Props = { pageParam?: number };
export async function getPostRecommends({ pageParam }: Props) {
const res = await fetch(
`http://localhost:9090/api/postRecommends?cursor=${pageParam}`,
{
next: {
tags: ["posts", "recommends"],
},
}
);
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
데이터 표시하기
useInfiniteQUery는 데이터를 페이지별로 따로 관리한다.
[[1, 2, 3, 4, 5],[6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]
데이터가 2차원 배열로 들어가있기 때문에 data.map으로 바로 접근할 수 없다.
따라서 data의 pages에 들어있는 배열을 하나씩 꺼낸 다음 다시 map으로 데이터를 꺼내 표시한다.
{data?.pages.map((page, i) => (
<Fragment key={i}>
{page.map((post) => (
<Post key={post.postId} post={post} />
))}
</Fragment>
))}
무한 스크롤링 구현하기
스크롤이 페이지의 가장 하단에 닿았을 때 getPostRecommend 함수를 실행시킨다.
이 동작은 Intersection Observer를 사용해 이벤트를 호출한다.
useInfiniteQuery에서 fetchNextPage와 hasNextPage를 꺼내온다.
- fetchNextPage : 데이터 요청 함수
- hasNextPage : 다음 페이지가 있는지를 나타내는 boolean값
- 데이터가 다 불러와진 경우 다음 페이지가 없으므로 false
const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery<
IPost[],
Object,
InfiniteData<IPost[]>,
[_1: string, _2: string],
number // initialPageParam 타입
>({
queryKey: ["posts", "recommends"],
queryFn: getPostRecommends,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.at(-1)?.postId,
staleTime: 60 * 1000,
});
react-intersection-observer를 설치하고 화면 위치를 감지할 수 있도록 div를 추가한다.
<>
{data?.pages.map((page, i) => (
<Fragment key={i}>
{page.map((post) => (
<Post key={post.postId} post={post} />
))}
</Fragment>
))}
<div style={{ height: 50 }} />
</>
div를 인식하면(화면에 나타나면) fetchNextPage 함수를 호출한다.
useInView 훅을 사용해 특정 컴포넌트가 화면에 나타날 경우 함수를 실행하도록 한다.
import { useInView } from "react-intersection-observer";
...
const { ref, inView } = useInView({
threshold: 0, // 화면에 몇 픽셀이 표시된 이후 이벤트가 발생하는지
delay: 1500, // 화면에 나타난 이후 몇 초뒤에 이벤트가 발생하는지
});
...
return (
<>
...
<div ref={ref} style={{ height: 50 }} />
</>
)
ref인 div 태그가 화면에 나타나면 inView는 true, 화면에서 사라지면 false가 된다.
useEffect로 inView가 변함에 따라 fetchNextPage 함수를 실행한다.
useEffect(() => {
if (inView) {
// 화면에 보일 떄
!isFetching && hasNextPage && fetchNextPage(); // 데이터를 가져오는 중이 아닐 떄 다음 페이지가 존재하면 데이터 요청
}
}, [inView, isFetching, hasNextPage, fetchNextPage]);
idFetching을 추가해 데이터를 가져오고 있는 상황이 아닌 경우에 데이터요청을 하도록 한다.
이로써 같은 데이터를 여러번 가져오는 실수를 줄일 수 있다.
'코딩 > 코딩노트' 카테고리의 다른 글
[React 상태 관리] Zustand로 상태 관리하기 (0) | 2024.07.17 |
---|---|
[ESLint, Prettier] ESLint와 Prettier에 대해 알아보자! (0) | 2024.07.16 |
백엔드에서 API를 아직 만들지 않았다면 MSW를 사용하자! - 회원가입 (0) | 2024.05.30 |
[Remix]Remix 공식문서 파헤치기 7탄 - 튜토리얼 진행하기 (0) | 2024.04.29 |
[Remix]Remix 공식문서 파헤치기 6탄 - 컴포넌트 (0) | 2024.04.26 |