ํฐ์คํ ๋ฆฌ ๋ทฐ
Tanstack Query + Context API + Zustand๋ก ์ ์ญ ์ํ ๊ด๋ฆฌํ๊ธฐ
*Context API : ์ํ๋ฅผ ์ ์ญ์ผ๋ก ๊ณต์
4์ฃผ์ฐจ ์์

๐ค Tanstack Query + Zustand ์ ๊ฐ์ด ์ธ๊น?
ํ ์คํ์ ์๋ฒ ์ํ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์บ์ฑํ๋ค.
์ฃผ์คํ ์ค๋ ํด๋ผ์ด์ธํธ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฐํธํ๊ฒ ํ ์ ์๋ค.
๋์ ํจ๊ป ์ฌ์ฉํ๋ฉด, ์๋ฒ ์ํ๋ฅผ ํด๋ผ์ด์ธํธ ์ํ๋ก ๋๊ธฐํํ๋ ํจํด์ ๋ง๋ค ์ ์๋ค.
์์ ์ํ ๋จ๊ณ
1. RQProvider.tsx ์์ ์๋ฒ/๋ธ๋ผ์ฐ์ ๊ฐ๊ฐ ์๋ก์ด ์ฟผ๋ฆฌ ํด๋ผ์ด์ธํธ ์์ฑ
"use client";
import {
isServer,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
// ์๋ฒ์ ๋ธ๋ผ์ฐ์ ๋ก ๋๋์ด ๋ณ๋์ ์ฟผ๋ฆฌ ํด๋ผ์ด์ธํธ ๋ถ์ฌ
function getQueryClient() {
if (isServer) {
return makeQueryClient();
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
2. Root layout์ provider๋ก ๊ฐ์ธ๊ธฐ
...
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Providers>{children}</Providers>
</body>
</html>
);
3. route.ts์์ ๋ผ์ฐํธ ํธ๋ค๋ฌ ์์ฑ
import { NextResponse } from "next/server";
export async function GET() {
const count = { value: 0 };
return NextResponse.json(count);
}
4. queryApi.ts์์ ๋ผ์ฐํธ ํธ๋ค๋ฌ ๊ฒฝ๋ก์์ ๋ฐ์ดํฐ ํ์นญ ํจ์ ์์ฑ
export const getCounts = async (): Promise<number> => {
const res = await fetch("/api/counts");
const data = await res.json();
return data;
};
5. page.tsx์์ ํ๋ฆฌํ์น์ฟผ๋ฆฌ๋ก ์ ์ html ๋ง๋ค์ด์ ์ฃผ์ (์ฟผ๋ฆฌํจ์ : ๋ฐ์ดํฐ ํ์นญ ํจ์)
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from "@tanstack/react-query";
import Counter from "./_components/counter";
import { getCounts } from "./utils/queryApi";
export default function Home() {
const queryClient = new QueryClient();
// ํ๋ฆฌํ์น
queryClient.prefetchQuery({
queryKey: ["count"],
queryFn: getCounts,
});
return (
// ํ๋ฆฌํ์น๋ฅผ ์ ์ html๋ก ๋ง๋ค์ด์ ์ฃผ์
<HydrationBoundary state={dehydrate(queryClient)}>
<Counter />
</HydrationBoundary>
);
}
6. countStore.ts์์ Zustand ์คํ ์ด ์์ฑ
import { create, StoreApi } from "zustand";
export type CountState = {
count: number;
increment: () => void;
decrement: () => void;
};
export const createCounterStore = create<CountState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
๐คTanstackProvider๋ root layout์์ ๊ฐ์๋๋ฐ, Zustand๋?
์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ์ Zustand Provider๊ฐ ํ์ ์๋ค.
- Provider ์์ด ์ ์ญ ์ํ ๊ด๋ฆฌ ๊ฐ๋ฅ
- ํ์ง๋ง ์๋ฒ ์ปดํฌ๋ํธ์ ์ฐ๋ํด์ผ ํ๊ฑฐ๋, ํน์ ์ปดํฌ๋ํธ๋ง ๋ ๋ฆฝ์ ์ธ Zustand ์ํ๋ฅผ ๊ฐ์ ธ์ผ ํ ๊ฒฝ์ฐ์๋ Provider๊ฐ ํ์ํจ.
- SSR์์ ์ด๊ธฐ ์ํ๋ฅผ ์ฃผ์ ํด์ผ ํ ๊ฒฝ์ฐ์๋ Provider๊ฐ ํ์ํ ์ ์์.
=> ๊ตณ์ด ์๋ฒ ์ปดํฌ๋ํธ์ ์ฐ๊ฒฐํ ํ์๊ฐ ์์ด์ Zustand Provider ์ ์ฉ ์ ํจ
๐ค counter.tsx์์ ์๋ฒ ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ useQuery ๊ฐ์ ์ฌ์ฉํ ์ง, Zustand Store๋ก ๊ฐ์ ๊ด๋ฆฌํ ์ง
์๋ฒ ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํ ์ง ๋ง์ง ๊ฒฐ์ ํ๊ธฐ
- ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ์ค์ ํ ์ด๊ธฐ๊ฐ (value : 0)์ useQuery์์ ์บ์ฑ ์ค
- ์๋ฒ์์ ์บ์ฑ ์ค์ธ ๊ฐ์ ์ฌ์ฉ(๊ทธ ๊ฐ์ผ๋ก ์ด๊ธฐํ)ํ๋ ค๋ฉด ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ useQuery์ useEffect ์ฌ์ฉ
- Store์์ ์ค์ ํ ์ด๊ธฐ๊ฐ์ ์ฌ์ฉํ๊ณ ํด๋ผ์ด์ธํธ ๋ด๋ถ์์๋ง ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ค๋ฉด Zustand๋ง ์ฌ์ฉ
=> ๋ง์ฐฌ๊ฐ์ง๋ก ํด๋ผ์ด์ธํธ์์๋ง ์ํ ๊ด๋ฆฌ ์ํด Zustand๋ง ์ฌ์ฉ
โ๏ธ Zustand Provider ์ฌ์ฉ ์ด์
- ์ปจํ ์คํธ(Context API)์ ํจ๊ป Zustand๋ฅผ ์ฌ์ฉํ ๋ ๊ด๋ฆฌ๊ฐ ํ์
- Provider ํจํด์ ์ฌ์ฉํ์ฌ Zustand ์ํ๋ฅผ Context๋ก ๊ฐ์ธ๋ ๋ฐฉ๋ฒ ์ฌ์ฉ
// CountProvider.tsx
// ์๋ฒ์์ Zustand ์ํ๋ฅผ ์ ๋ฌํด์ผ ํ๋ ๊ฒฝ์ฐ Provider ํ์
"use client";
import { CountState, createCounterStore } from "@/countStore";
import { createContext, ReactNode, useRef } from "react";
import { StoreApi } from "zustand";
// ์ด๊ธฐ๊ฐ null : ์ธ๋ถ์์ ์ฌ์ฉ ์ ์ค๋ฅ ๋ฐ์
export const CountContext = createContext<StoreApi<CountState> | null>(null);
export type CounterProviderProps = {
children: ReactNode; // Provider ๋ด๋ถ์ ๋ชจ๋ JSX ์์ ๊ฐ๋ฅ
};
export function CountProvider({ children }: CounterProviderProps) {
// useRef : Zustand ์ํ(storeRef.current) ํ ๋ฒ๋ง ์์ฑ(๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์ง)
const storeRef = useRef<StoreApi<CountState> | null>(null);
// ์คํ ์ด๊ฐ ์์ ๋ ์๋กญ๊ฒ ์์ฑ(์ฒ์ ๋ง์ดํธ ์) / ์ด๋ฏธ ์๋ค๋ฉด ๊ธฐ์กด ์ํ ์ ์ง
if (!storeRef.current) {
storeRef.current = createCounterStore();
}
return (
// ํ์ ์ปดํฌ๋ํธ์๊ฒ ํ์ฌ Zustand ์ํ ์ ๋ฌ
<CountContext.Provider value={storeRef.current}>
{children}
</CountContext.Provider>
);
}
- createCounterStore: Zustand ์ํ๋ฅผ ์์ฑํ๋ ํจ์ (countStore.ts์์ ์ ์)
- CountState: Zustand ์ํ์ ํ์ ์ ์ (countStore.ts์์ ๊ฐ์ ธ์ด)
- createContext: React์ Context API๋ก ์ํ๋ฅผ ์ ์ญ์ผ๋ก ๊ณต์ ํ ๋ ์ฌ์ฉ
- ReactNode: children์ ํ์ ์ผ๋ก ์ฌ์ฉ (Provider ์์ ์ด๋ค JSX ์์๋ ์ฌ ์ ์๋๋ก ์ค์ )
- useRef: React์ ์ฐธ์กฐ(ref) ํ ์ผ๋ก ํ ๋ฒ๋ง Zustand ์คํ ์ด๋ฅผ ์์ฑํ๋๋ก ๋ณด์ฅ
- StoreApi: Zustand์ create ํจ์๊ฐ ๋ฐํํ๋ ์คํ ์ด ๊ฐ์ฒด์ ํ์
'Language > Next' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [์ฌ๋๋ณ] Next.js ์๋ฌ ํธ๋ค๋ง (error.tsx) (5) | 2025.04.26 |
|---|---|
| [/\/] Route Handler, Server-Action (0) | 2025.03.22 |
| [/\/] Next.js์์ ๋งํ๋ '์๋ฒ'๋? (0) | 2025.03.19 |
| [/\/] Error Handling (0) | 2025.03.19 |
| [/\/] Next์ Tanstack Query (0) | 2025.03.19 |