Sonamu์์ TanStack Query์ ํด๋ผ์ด์ธํธ ์บ์ฑ๊ณผ SSR ์๋ต์ HTTP ์บ์ฑ์ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ด
๋๋ค.
์บ์ ์ ์ด ๊ฐ์
TanStack Query
ํด๋ผ์ด์ธํธ ๋ฉ๋ชจ๋ฆฌ ์บ์ฑ staleTime/gcTime
HTTP ์บ์
Cache-Control ํค๋ CDN/๋ธ๋ผ์ฐ์ ์บ์ฑ
์ฌ๊ฒ์ฆ
์๋/์๋ ๊ฐฑ์ ์ต์ ๋ฐ์ดํฐ ์ ์ง
์ฑ๋ฅ ์ต์ ํ
๋น ๋ฅธ ์๋ต ์๋ฒ ๋ถํ ๊ฐ์
TanStack Query ํด๋ผ์ด์ธํธ ์บ์ฑ
๊ธฐ๋ณธ ์ค์
// entry-client.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ์บ์ ๊ด๋ จ ์ค์
staleTime: 5000, // 5์ด (๋ฐ์ดํฐ๊ฐ freshํ ์๊ฐ)
gcTime: 5 * 60 * 1000, // 5๋ถ (๊ฐ๋น์ง ์ปฌ๋ ์
์๊ฐ)
// ์ฌ๊ฒ์ฆ ์ค์
refetchOnWindowFocus: false, // ์๋์ฐ ํฌ์ปค์ค ์ ์ฌ๊ฒ์ฆ ์ ํจ
refetchOnMount: true, // ๋ง์ดํธ ์ ์ฌ๊ฒ์ฆ
refetchOnReconnect: true, // ์ฌ์ฐ๊ฒฐ ์ ์ฌ๊ฒ์ฆ
// ์ฌ์๋ ์ค์
retry: false, // ์คํจ ์ ์ฌ์๋ ์ ํจ
},
},
});
staleTime vs gcTime
// staleTime: ๋ฐ์ดํฐ๊ฐ "์ ์ ํ" ์๊ฐ
// โ ์ด ์๊ฐ ๋์์ ์ฌ์์ฒญํ์ง ์์
// โ 0์ด๋ฉด ํญ์ stale (๊ธฐ๋ณธ๊ฐ)
// gcTime: ์บ์์ ๋ณด๊ดํ๋ ์๊ฐ (๊ตฌ cacheTime)
// โ ์ด ์๊ฐ ๋์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ง
// โ 5๋ถ (300000ms)์ด ๊ธฐ๋ณธ๊ฐ
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: fetchUser,
staleTime: 5000, // 5์ด๊ฐ fresh
gcTime: 10 * 60 * 1000, // 10๋ถ๊ฐ ์บ์ ์ ์ง
});
// ํ์๋ผ์ธ:
// 0์ด: ๋ฐ์ดํฐ fetch โ fresh
// 5์ด: stale ์ํ๋ก ์ ํ
// (ํ์ง๋ง ์บ์์๋ ์์ง ์์)
// ์ฌ์์ฒญ ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ refetch
// 10๋ถ: ์บ์์์ ์ ๊ฑฐ (๊ฐ๋น์ง ์ปฌ๋ ์
)
์ฟผ๋ฆฌ๋ณ ์บ์ ์ค์
Service Hook์ ์ต์
์ ์ ๋ฌํ์ฌ ๊ฐ๋ณ ์ค์ ํ ์ ์์ต๋๋ค.
import { UserService } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
const { data } = UserService.useUser("C", userId, {
// ์ด ์ฟผ๋ฆฌ๋ง์ ์ค์
staleTime: 10 * 60 * 1000, // 10๋ถ fresh
gcTime: 30 * 60 * 1000, // 30๋ถ ์บ์ ์ ์ง
refetchOnWindowFocus: true, // ํฌ์ปค์ค ์ ์ฌ๊ฒ์ฆ
});
return <div>{data?.user.username}</div>;
}
๋ฐ์ดํฐ ํน์ฑ๋ณ ์บ์ฑ ์ ๋ต
// 1. ์ค์๊ฐ ๋ฐ์ดํฐ (์ฃผ์, ์ฑํ
๋ฑ)
const { data } = useQuery({
queryKey: ["stock", symbol],
queryFn: fetchStock,
staleTime: 0, // ํญ์ stale
refetchInterval: 5000, // 5์ด๋ง๋ค ์๋ refetch
});
// 2. ์์ฃผ ๋ณ๊ฒฝ๋๋ ๋ฐ์ดํฐ (ํผ๋, ์๋ฆผ ๋ฑ)
const { data } = useQuery({
queryKey: ["feed"],
queryFn: fetchFeed,
staleTime: 60 * 1000, // 1๋ถ fresh
refetchOnWindowFocus: true,
});
// 3. ๊ฐ๋ ๋ณ๊ฒฝ๋๋ ๋ฐ์ดํฐ (ํ๋กํ, ์ค์ ๋ฑ)
const { data } = useQuery({
queryKey: ["profile"],
queryFn: fetchProfile,
staleTime: 5 * 60 * 1000, // 5๋ถ fresh
refetchOnWindowFocus: false,
});
// 4. ๊ฑฐ์ ์ ๋ณํ๋ ๋ฐ์ดํฐ (์นดํ
๊ณ ๋ฆฌ, ๊ตญ๊ฐ ๋ชฉ๋ก ๋ฑ)
const { data } = useQuery({
queryKey: ["categories"],
queryFn: fetchCategories,
staleTime: Infinity, // ์์ํ fresh
gcTime: Infinity, // ์์ํ ์บ์ ์ ์ง
});
์๋ ์บ์ ์ ์ด
์บ์ ๋ฌดํจํ
"use client";
import { useQueryClient } from "@tanstack/react-query";
import { PostService } from "@/services/services.generated";
export function CreatePostForm() {
const queryClient = useQueryClient();
async function handleSubmit(data: any) {
// ๊ฒ์๊ธ ์์ฑ
await PostService.createPost(data);
// ๊ด๋ จ ์บ์ ๋ฌดํจํ
queryClient.invalidateQueries({
queryKey: ["Post", "list"], // ๊ฒ์๊ธ ๋ชฉ๋ก ๋ฌดํจํ
});
// ๋๋ ํน์ ์ฟผ๋ฆฌ๋ง
queryClient.invalidateQueries({
queryKey: ["Post", "list", { page: 1 }],
});
}
return <form onSubmit={handleSubmit}>...</form>;
}
์บ์ ์ง์ ์
๋ฐ์ดํธ
"use client";
import { useQueryClient } from "@tanstack/react-query";
export function LikeButton({ postId }: { postId: number }) {
const queryClient = useQueryClient();
async function handleLike() {
// 1. ํ์ฌ ์บ์ ๊ฐ์ ธ์ค๊ธฐ
const queryKey = ["Post", "getPost", "C", postId];
const previousPost = queryClient.getQueryData(queryKey);
// 2. ๋๊ด์ ์
๋ฐ์ดํธ
queryClient.setQueryData(queryKey, (old: any) => ({
...old,
post: {
...old.post,
likes: old.post.likes + 1,
},
}));
try {
// 3. ์๋ฒ์ ์์ฒญ
await PostService.likePost(postId);
} catch (error) {
// 4. ์คํจ ์ ๋กค๋ฐฑ
queryClient.setQueryData(queryKey, previousPost);
}
}
return <button onClick={handleLike}>Like</button>;
}
์บ์ ์กฐํ
const queryClient = useQueryClient();
// ๋จ์ผ ์ฟผ๋ฆฌ ์กฐํ
const userData = queryClient.getQueryData(["User", "getUser", "C", 123]);
// ๋ชจ๋ ์ฟผ๋ฆฌ ์กฐํ
const allQueries = queryClient.getQueryCache().getAll();
// ํน์ ํจํด ์ฟผ๋ฆฌ ์กฐํ
const postQueries = queryClient.getQueryCache().findAll({ queryKey: ["Post"] });
SSR Cache-Control
registerSSR์์ HTTP Cache-Control ํค๋๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
๊ฐ๋ณ ๋ผ์ฐํธ ์บ์ฑ
// api/src/application/sonamu.ts
import { registerSSR } from "sonamu";
registerSSR({
path: "/posts/:id",
preload: (params) => [
/* ... */
],
cacheControl: {
maxAge: 3600, // 1์๊ฐ (๋ธ๋ผ์ฐ์ ์บ์)
sMaxAge: 7200, // 2์๊ฐ (CDN ์บ์)
staleWhileRevalidate: 86400, // 1์ผ๊ฐ stale ์ปจํ
์ธ ์ ๊ณต ๊ฐ๋ฅ
public: true, // ๊ณต๊ฐ ์บ์ ํ์ฉ
},
});
์์ฑ๋๋ ํค๋:
Cache-Control: public, max-age=3600, s-maxage=7200, stale-while-revalidate=86400
์บ์ฑ ์ ๋ต ์์
// 1. ์ ์ ์ปจํ
์ธ (๊ธด ์บ์ฑ)
registerSSR({
path: "/about",
cacheControl: {
maxAge: 86400, // 1์ผ
sMaxAge: 604800, // 7์ผ (CDN)
public: true,
},
});
// 2. ๋์ ์ปจํ
์ธ (์งง์ ์บ์ฑ)
registerSSR({
path: "/posts/:id",
cacheControl: {
maxAge: 300, // 5๋ถ
sMaxAge: 600, // 10๋ถ (CDN)
staleWhileRevalidate: 3600,
public: true,
},
});
// 3. ๊ฐ์ธํ ์ปจํ
์ธ (์บ์ฑ ์ ํจ)
registerSSR({
path: "/dashboard",
cacheControl: {
noStore: true, // ์บ์ ์์ ๋นํ์ฑํ
private: true, // ๊ฐ์ธ ์ ์ฉ
},
});
// 4. ์บ์ ์์ (๋งค๋ฒ ์ฌ๊ฒ์ฆ)
registerSSR({
path: "/live-feed",
cacheControl: {
noCache: true, // ์ฌ๊ฒ์ฆ ํ์
},
});
CacheControlConfig ํ์
type CacheControlConfig = {
maxAge?: number; // ๋ธ๋ผ์ฐ์ ์บ์ ์๊ฐ (์ด)
sMaxAge?: number; // CDN ์บ์ ์๊ฐ (์ด)
staleWhileRevalidate?: number; // stale ์ปจํ
์ธ ์ ๊ณต ์๊ฐ (์ด)
staleIfError?: number; // ์๋ฌ ์ stale ์ปจํ
์ธ ์ ๊ณต ์๊ฐ (์ด)
public?: boolean; // ๊ณต๊ฐ ์บ์ ํ์ฉ
private?: boolean; // ๊ฐ์ธ ์ ์ฉ (CDN ์บ์ ์ ํจ)
noCache?: boolean; // ์ฌ๊ฒ์ฆ ํ์
noStore?: boolean; // ์บ์ ์์ ๋นํ์ฑํ
mustRevalidate?: boolean; // ์ฌ๊ฒ์ฆ ๊ฐ์
immutable?: boolean; // ๋ถ๋ณ ๋ฆฌ์์ค
};
์ ์ญ ์บ์ฑ ํธ๋ค๋ฌ
๋ชจ๋ SSR ๋ผ์ฐํธ์ ์ ์ฉ๋๋ ์ ์ญ ํธ๋ค๋ฌ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
// api/src/application/sonamu.ts
import { SonamuFastifyConfig } from "sonamu";
const config: SonamuFastifyConfig = {
cacheControlHandler: (req) => {
// SSR ์์ฒญ ์ ๋ณด
const { type, url, path, method } = req;
// ์ ์ ํ์ด์ง
if (path === "/about" || path === "/terms") {
return {
maxAge: 86400, // 1์ผ
public: true,
};
}
// ๋์ ํ์ด์ง
if (path.startsWith("/posts/")) {
return {
maxAge: 300, // 5๋ถ
staleWhileRevalidate: 3600,
public: true,
};
}
// ๊ธฐ๋ณธ๊ฐ: ์บ์ฑ ์ ํจ
return {
noCache: true,
};
},
};
ํ์ด๋ธ๋ฆฌ๋ ์บ์ฑ
SSR๊ณผ ํด๋ผ์ด์ธํธ ์บ์ฑ์ ํจ๊ป ์ฌ์ฉํฉ๋๋ค.
// ์๋ฒ: HTTP ์บ์ฑ (1์๊ฐ)
registerSSR({
path: "/posts/:id",
cacheControl: {
maxAge: 3600,
},
});
// ํด๋ผ์ด์ธํธ: ๋ฉ๋ชจ๋ฆฌ ์บ์ฑ (5๋ถ)
const { data } = PostService.usePost("C", postId, {
staleTime: 5 * 60 * 1000,
});
์บ์ฑ ๋ ์ด์ด:
- ๋ธ๋ผ์ฐ์ HTTP ์บ์: 1์๊ฐ (max-age=3600)
- TanStack Query ๋ฉ๋ชจ๋ฆฌ: 5๋ถ (staleTime)
- ์๋ฒ SSR ๋ ๋๋ง: ์บ์ ๋ฏธ์ค ์ ๋ ๋๋ง
ํ๋ฆ:
- ์ฒซ ๋ฐฉ๋ฌธ: SSR โ HTML + ๋ฐ์ดํฐ โ ๋ธ๋ผ์ฐ์ ์บ์ ์ ์ฅ
- 5๋ถ ์ด๋ด ์ฌ๋ฐฉ๋ฌธ: TanStack Query ๋ฉ๋ชจ๋ฆฌ ์บ์ ์ฌ์ฉ
- 5๋ถ~1์๊ฐ: ๋ธ๋ผ์ฐ์ HTTP ์บ์ ์ฌ์ฉ
- 1์๊ฐ ์ดํ: ์๋ฒ์ ์๋ก ์์ฒญ
๊ฐ๋ฐ ํ๊ฒฝ ๋๋ฒ๊น
// web/src/routes/__root.tsx
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function RootComponent() {
return (
<html>
<body>
<QueryClientProvider client={queryClient}>
<Outlet />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</body>
</html>
);
}
์บ์ ์ํ ํ์ธ
"use client";
import { useQueryClient } from "@tanstack/react-query";
export function CacheDebugger() {
const queryClient = useQueryClient();
function showCache() {
const cache = queryClient.getQueryCache();
console.log("Total queries:", cache.getAll().length);
cache.getAll().forEach((query) => {
console.log({
queryKey: query.queryKey,
state: query.state.status,
dataUpdatedAt: new Date(query.state.dataUpdatedAt),
staleTime: query.options.staleTime,
});
});
}
return <button onClick={showCache}>Show Cache</button>;
}
HTTP ์บ์ ํค๋ ํ์ธ
๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ โ Network ํญ์์ ์๋ต ํค๋ ํ์ธ:
Cache-Control: public, max-age=3600, s-maxage=7200, stale-while-revalidate=86400
Age: 1234
X-Cache: HIT
์ฑ๋ฅ ์ต์ ํ ์ ๋ต
1. Subset ํ์ฉ
ํ์ํ ํ๋๋ง ์บ์ํ์ฌ ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ:
// โ ์ ์ฒด ํ๋ ์บ์ (ํผ)
UserService.useUser("C", userId, {
staleTime: 5 * 60 * 1000,
});
// โ
ํ์ํ ํ๋๋ง ์บ์ (์์)
UserService.useUser("A", userId, {
staleTime: 5 * 60 * 1000,
});
2. ์ ํ์ ์ฌ๊ฒ์ฆ
์ค์ํ ๋ฐ์ดํฐ๋ง ์ ๊ทน์ ์ผ๋ก ์ฌ๊ฒ์ฆ:
// ์ค์ํ ๋ฐ์ดํฐ
const { data: user } = UserService.useUser("C", userId, {
refetchOnWindowFocus: true,
refetchOnMount: true,
});
// ๋ ์ค์ํ ๋ฐ์ดํฐ
const { data: posts } = PostService.usePosts("A", {
refetchOnWindowFocus: false,
refetchOnMount: false,
});
3. ์บ์ ํ๋ฆฌํ์นญ
๋ค์์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ์บ์ฑ:
const queryClient = useQueryClient();
function prefetchPost(postId: number) {
queryClient.prefetchQuery({
queryKey: ["Post", "getPost", "C", postId],
queryFn: () => PostService.getPost("C", postId),
});
}
// ๋งํฌ์ ๋ง์ฐ์ค ์ฌ๋ฆฌ๋ฉด ํ๋ฆฌํ์นญ
<Link
to={`/posts/${postId}`}
onMouseEnter={() => prefetchPost(postId)}
>
Read more
</Link>
์ฃผ์์ฌํญ
์บ์ ์ฌ์ฉ ์ ์ฃผ์์ฌํญ: 1. staleTime ์ค์ ํ์: ๊ธฐ๋ณธ๊ฐ 0์ ํญ์ refetch 2. ๊ฐ์ธ ๋ฐ์ดํฐ๋
private: public ์บ์ ๊ธ์ง 3. ๋ฏผ๊ฐํ ๋ฐ์ดํฐ: noStore๋ก ์บ์ฑ ๋นํ์ฑํ 4. SSR cacheControl:
๊ฐ๋ณ ์ค์ ์ด ์ ์ญ ํธ๋ค๋ฌ๋ณด๋ค ์ฐ์ 5. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ: gcTime์ผ๋ก ๋ถํ์ํ ์บ์ ์ ๊ฑฐ
๋ค์ ๋จ๊ณ
SSR ์ค์
SSR ๊ธฐ๋ณธ ๊ตฌ์กฐ
๋ฐ์ดํฐ ํ๋ฆฌ๋ก๋ฉ
registerSSR ์ฌ์ฉ๋ฒ
Hydration ์ ๋ต
ํ์ด๋๋ ์ด์
์ต์ ํ
TanStack Query ๊ณต์ ๋ฌธ์
Query ์์ธ ๊ฐ์ด๋