우리는 외부에서 데이터를 가져와서 사용해야 하기 때문에 이를 위한 관리 도구가 필요하다. 이전에 다루었던 것처럼 이번에도 역시 TanStack Query(React Query)를 사용할 것이다.
참고로 Next.js에서는 `fetch`를 최적화해서 제공하기 때문에 `axios`를 사용하지 않고 `fetch`를 사용할 것이다.
세팅 : app route 사용, src 폴더 사용
1. `server-action.ts` 만들기
먼저 server component를 만들었던 것처럼 파일 맨 상단에 `"use server"`를 추가하자. 그러면 해당 `server action` 파일에 비동기 함수를 모아 api처럼 활용할 수 있다. 이러한 `server-action.ts` 파일은 서버 측에서 비동기 함수를 실행할 수 있게 해준다.
`src` 폴더에 `server-action.ts`를 만들고 데이터를 불러오는 함수를 만들자.
// ./db.json
{
"db": [
{
"id": "1",
"count": 0
}
]
}
// ./src/types/type.ts
export type DB = { count: number };
// ./src/server-action.ts
"use server";
import { DB } from "./types/type";
const BASE_URL = "http://localhost:5000";
export async function getData(): Promise<DB> {
const res = await fetch(BASE_URL + "/db", {
cache: 'no-store'
});
const data: DB[] = await res.json();
return data[0];
}
export async function update(count: number): Promise<void> {
const res = await fetch(BASE_URL + "/db/1", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({count}),
cache: 'no-store',
});
}
여기서 `db.json`을 살펴보면 불필요해보이는 `id`가 있는 것을 알 수 있는데, `json-server` docs에 의해 `PATCH`는 `id`를 필요로하기 때문이다. 그리고 맨 처음에 `db`를 배열로 만들지 않고 `{"count": 0`}`이라고 했다가 데이터가 이상하게 저장되는 문제를 겪었다.
2. 데이터 불러오기 : `useQuery` 사용하기
// ./src/providers/TQProvider.tsx
"use client";
// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
isServer,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
// ./src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import QCProvider from "@/components/providers/TQProvider";
import { DB } from "@/types/type";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const res = await fetch("http://localhost:5000/db");
const data: DB[] = await res.json();
return (
<html lang="en">
<body>
<QCProvider>
{children}
</QCProvider>
</body>
</html>
);
}
// ./src/components/Display.tsx
"use client";
import { getData } from "@/server-action";
import { DB } from "@/types/type";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
const Display = () => {
const queryClient = useQueryClient();
const {
data = {count},
isLoading,
isPending,
}: { data: DB | undefined; isLoading: Boolean; isPending: Boolean } =
useQuery({
queryKey: ["count"],
queryFn: () => getData(),
});
const { mutate } = useMutation({
mutationFn: update,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["count"],
});
},
});
if (isLoading || isPending) return <>loading.....</>;
return (
<div className="flex flex-col justify-center items-center">
<p>
Count : <span>{data?.count}</span>
</p>
<div className="flex gap-4">
<button
className="px-5 py-2 bg-gray-300 rounded-md"
onClick={() => {
mutate(data.count + 1);
}}
>
+1
</button>
<button
className="px-5 py-2 bg-gray-300 rounded-md"
onClick={() => {
mutate(data.count - 1);
}}
>
-1
</button>
<button
className="px-5 py-2 bg-gray-300 rounded-md"
onClick={() => {
mutate(0);
}}
>
초기화
</button>
</div>
</div>
);
};
export default Display;
여기서 주목해야할 부분은 `queryFn`부분이다. TypeScript를 사용하지 않았을 때에는 `getData`만 적었을텐데(이와 같은 방법을 refernce calling이라고 한다) TS를 사용할 때에는 함수로 실행시켜줘야 한다. 따라서 `queryFn: () => getData()`로 적어야 한다.
TanStack Query Docs를 확인해보면 data fetching 함수들이 대부분 return type이 `any`라서 type을 명확히 하기 위함이라고 한다. 원래 이부분도 `queryFn: getData`이라고 했다가 데이터 로딩이 너무 느리고, `invalidateQueries`가 제대로 작동하지 않는 문제를 겪었다.
3. `prefetchQuery` 사용하기
위 `Display.tsx`는 `"use client"`가 적혀있는 것을 보면, client component인 것을 알 수 있다. 따라서 브라우저에서 렌더링될 때 데이터를 불러오게 된다. 그러면 데이터가 많은 경우 로딩이 길어져 TTV(Time-To-View)가 저해된다. 따라서 server component에서 미리 데이터를 불러와서 전달해주는 것이 좋을 것이다.
이를 위해서 TanStack Query를 사용해 데이터를 prefetchg할 수 있는 방법이 있다.
// ./src/app/page.tsx
import Display from "@/components/Display";
import { getData } from "@/server-action";
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from "@tanstack/react-query";
export default async function Home() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000,
},
},
});
await queryClient.prefetchQuery({
queryKey: ["count"],
queryFn: () => getData(),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className='flex flex-col justify-center items-center'>
<h3>Counting</h3>
<Display />
</div>
</HydrationBoundary>
);
}
위와 같이 `Display.tsx`의 상위 컴포넌트에서 같은 `queryKey`를 갖도록 prefetch해주고 하위 컴포넌트를 `HydrationBoundary`로 감싸주면 된다. 여기서도 `quefyFn`이 함수 형태로 작성되었음을 확인하자.
이번에는 굉장히 사소한 부분에서 에러를 많이 겪어서 스스로 발견하기가 너무 어려웠다. 이번 기회로 머리에 잘 넣어 둘 수 있게 되었다!
'Frontend > Today I Learned' 카테고리의 다른 글
[Next.js] 쉬운 성능 최적화 방법 소개 (0) | 2024.10.11 |
---|---|
[Next.js] Middleware로 잘못된 경로 접근 시 redirect하기 (2) | 2024.10.03 |
[Next.js] React Server Component & Client Component (1) | 2024.09.30 |
[TS] TypeScript 시작하기 - 2 (1) | 2024.09.25 |
[TS] TypeScript 시작하기 (4) | 2024.09.24 |