
📅 2025-12-12
➡️ Next.js Tanstack Query, Zustand에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리
useQuery : 데이터 가져올 때 사용
useQuery({
queryKey: ["books"],
queryFn: getBooks,
staleTime: 60_000,
gcTime: 60_000,
});
queryKey → 필수 인자 값
queryFn → 필수 인자 값
Promise를 반환하는 함수staleTime
gcTime
useMutation : 데이터 생성(Create), 수정(Update), 삭제(Delete) 등의 작업에 사용const { mutate, isPending, isError } = useMutation({
mutationFn: async (payload) => {
const res = await axios.post("/api/data", payload);
return res.data;
},
onSuccess: () => {
// 성공했을 때 후처리
},
onError: () => {
// 실패했을 때 처리
},
});onSuccess, onError 메서드를 통해 성공, 실패했을 시 res 데이터를 핸들링 할 수 있음🔗 React TanStack Query Nextjs Suspense Streaming Example | TanStack Query Docs
설치
npm i @tanstack/react-query @tanstack/react-query-devtools
Provider 구성
// In Next.js, this file would be called: app/providers.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";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
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}>
<ReactQueryDevtools />
{children}
</QueryClientProvider>
);
}
QueryClientProvider는 내부적으로 useContext 훅을 사용하기 때문에 "use client"로 클라이언트 컴포넌트 선언해야 함makeQueryClient()QueryClient 인스턴스를 생성하는 함수staleTime을 1분으로 설정하여 데이터가 일정 시간 동안 신선한 상태로 유지되도록 함refetch가 발생하는 문제를 줄이는 데 도움이 됨layout에 Provider 적용import Providers from "./providers";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}prefetchQuery()로 데이터 미리 요청dehydrate()QueryClient 상태 직렬화HydrationBoundaryuseQueryimport { QueryClient, dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getBooks } from "@/server-action";
import BookList from "@/components/BookList";
export default async function Page() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["books"],
queryFn: getBooks,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<BookList />
</HydrationBoundary>
);
}
"use client"는 클라이언트 상호작용을 포함한다는 의미이지, 컴포넌트 전체가 CSR로 바뀐다는 뜻이 아님create 하나로 스토어를 만들고, Hook처럼 불러다 쓰는 구조🔗 Setup with Next.js - Zustand
설치
npm i zustand
타입 및 store 정의
import { Book } from "@/type/book";
import { createStore } from "zustand";
export type CartBook = Book & { quantity: number };
export interface CartProps {
books: CartBook[];
}
export interface CartState extends CartProps {
addBook: (book: CartBook) => void;
}
export type CartStore = ReturnType<typeof createCartStore>;
export const createCartStore = (initProps?: Partial<CartProps>) => {
const DEFAULT_PROPS: CartProps = {
books: [],
};
return createStore<CartState>()((set) => ({
...DEFAULT_PROPS,
...initProps,
addBook: (book: CartBook) =>
set((state) => ({
books: [...state.books, book],
})),
}));
};
Provider 설정
type CartProviderProps = React.PropsWithChildren<CartProps>;
export function CartProvider({ children, ...props }: CartProviderProps) {
const storeRef = useRef<CartStore>();
if (!storeRef.current) {
storeRef.current = createCartStore(props);
}
return (
<CartContext.Provider value={storeRef.current}>
{children}
</CartContext.Provider>
);
}
사용법
const { books: items, remove } = useCartStore((state) => state);