Sonamu์์ ์๋ฒ ๋ ๋๋ง๋ HTML์ ํด๋ผ์ด์ธํธ์์ ์ธํฐ๋ํฐ๋ธํ๊ฒ ๋ง๋๋ ํ์ด๋๋ ์ด์
์ ๋ต์ ์์๋ด
๋๋ค.
Hydration ๊ฐ์
TanStack Query
dehydrate/hydrate ์๋ ์ํ ๋ณต์
์ ์ฒด ๋ฌธ์ Hydration
document ๋ ๋ฒจ ์์ ํ SSR
Date ์ง๋ ฌํ
dateReviver ์๋ ๋ณํ
์ ํ์ ๋นํ์ฑํ
disableHydrate CSR ์ ํ
Sonamu Hydration ํ๋ก์ธ์ค
1. ์๋ฒ: Dehydration
์๋ฒ์์ Query์ํ๋ฅผ ์ง๋ ฌํํฉ๋๋ค.
// entry-server.generated.tsx
import { dehydrate } from "@tanstack/react-query";
export async function render(url: string, preloadedData: PreloadedData[] = []) {
const queryClient = new QueryClient();
// ๋ฐ์ดํฐ ์ฃผ์
for (const { queryKey, data } of preloadedData) {
queryClient.setQueryData(queryKey, data);
}
// Dehydrate: QueryClient ์ํ๋ฅผ JSON์ผ๋ก ์ง๋ ฌํ
const dehydratedState = dehydrate(queryClient);
const appHtml = renderToString(<RouterProvider router={router} />);
return {
html: appHtml,
dehydratedState, // ์ง๋ ฌํ๋ ์ํ
};
}
2. HTML์ ๋ฐ์ดํฐ ์ฃผ์
// sonamu/src/ssr/renderer.ts
const ssrDataScript = dehydratedState
? `<script>window.__SONAMU_SSR__ = ${JSON.stringify(dehydratedState).replace(/</g, "\\u003c")};</script>`
: "";
// </body> ์ง์ ์ ์ฃผ์
const finalHtml = fullDocHtml.replace("</body>", `${ssrDataScript}\n${viteScripts}\n</body>`);
์์ฑ๋ HTML:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div id="root">
<!-- ์๋ฒ ๋ ๋๋ง๋ HTML -->
<div>Welcome, John</div>
</div>
<!-- SSR ๋ฐ์ดํฐ -->
<script>
window.__SONAMU_SSR__ = {
queries: [
{
queryKey: ["User", "getUser", "C", 123],
queryHash: '["User","getUser","C",123]',
state: {
data: {
user: {
id: 123,
username: "John",
createdAt: "2024-01-01T00:00:00.000Z",
},
},
dataUpdateCount: 1,
dataUpdatedAt: 1704067200000,
status: "success",
},
},
],
};
</script>
<!-- Vite ์คํฌ๋ฆฝํธ -->
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>
3. ํด๋ผ์ด์ธํธ: Hydration
ํด๋ผ์ด์ธํธ์์ ์ํ๋ฅผ ๋ณต์ํ๊ณ React๋ฅผ ํ์ฑํํฉ๋๋ค.
// entry-client.tsx
import { hydrate } from "@tanstack/react-query";
import { dateReviver } from "./services/sonamu.shared";
// 1. window.__SONAMU_SSR__์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
const dehydratedState = window.__SONAMU_SSR__
? JSON.parse(JSON.stringify(window.__SONAMU_SSR__), dateReviver)
: undefined;
// 2. QueryClient์ hydrate
if (dehydratedState) {
hydrate(queryClient, dehydratedState);
}
// 3. React Hydration
if (document.documentElement.innerHTML && dehydratedState) {
// SSR ํ์ด์ง - Hydration
ReactDOM.hydrateRoot(document, <RouterProvider router={router} />);
} else {
// Pure CSR ํ์ด์ง - ์๋ก ๋ ๋๋ง
ReactDOM.createRoot(document).render(<RouterProvider router={router} />);
}
Date ์ง๋ ฌํ ์ฒ๋ฆฌ
๋ฌธ์ : Date ๊ฐ์ฒด๋ JSON.stringify๋ก ๋ฌธ์์ด์ด ๋จ
const user = {
id: 123,
username: "John",
createdAt: new Date("2024-01-01"), // Date ๊ฐ์ฒด
};
// ์๋ฒ์์ ์ง๋ ฌํ
JSON.stringify(user);
// '{"id":123,"username":"John","createdAt":"2024-01-01T00:00:00.000Z"}'
// โ ๋ฌธ์์ด!
// ํด๋ผ์ด์ธํธ์์ ์ญ์ง๋ ฌํ
JSON.parse(json);
// { id: 123, username: "John", createdAt: "2024-01-01T00:00:00.000Z" }
// โ ์ฌ์ ํ ๋ฌธ์์ด!
ํด๊ฒฐ: dateReviver
Sonamu๋ ์๋์ผ๋ก ISO ๋ ์ง ๋ฌธ์์ด์ Date ๊ฐ์ฒด๋ก ๋ณํํฉ๋๋ค.
// services/sonamu.shared.ts
export function dateReviver(_key: string, value: unknown): unknown {
if (typeof value === "string") {
// ISO 8601 ๋ ์ง ๋ฌธ์์ด ํจํด
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
if (isoDatePattern.test(value)) {
return new Date(value); // Date ๊ฐ์ฒด๋ก ๋ณํ
}
}
return value;
}
// entry-client.tsx
const dehydratedState = window.__SONAMU_SSR__
? JSON.parse(JSON.stringify(window.__SONAMU_SSR__), dateReviver)
: undefined;
// โ
createdAt์ด Date ๊ฐ์ฒด๋ก ๋ณต์๋จ!
Hydration Mismatch ๋ฐฉ์ง
๋ฌธ์ : ์๋ฒ์ ํด๋ผ์ด์ธํธ์ HTML์ด ๋ค๋ฅด๋ฉด ์๋ฌ
// โ Hydration Mismatch ๋ฐ์
function Clock() {
const [time, setTime] = useState(new Date());
// ์๋ฒ์ ํด๋ผ์ด์ธํธ์ ์๊ฐ์ด ๋ค๋ฆ!
return <div>{time.toLocaleTimeString()}</div>;
}
์๋ฌ ๋ฉ์์ง:
Warning: Text content did not match. Server: "10:30:45" Client: "10:30:47"
ํด๊ฒฐ 1: useEffect ์ฌ์ฉ
// โ
ํด๋ผ์ด์ธํธ์์๋ง ์คํ
function Clock() {
const [time, setTime] = useState<Date | null>(null);
useEffect(() => {
setTime(new Date());
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
if (!time) return <div>--:--:--</div>; // ์๋ฒ ๋ ๋๋ง
return <div>{time.toLocaleTimeString()}</div>; // ํด๋ผ์ด์ธํธ๋ง
}
ํด๊ฒฐ 2: suppressHydrationWarning
์๋์ ์ผ๋ก ์๋ฒ/ํด๋ผ์ด์ธํธ๊ฐ ๋ค๋ฅธ ๊ฒฝ์ฐ ์ฌ์ฉํฉ๋๋ค.
function RandomQuote() {
const [quote] = useState(() => getRandomQuote());
return (
<div suppressHydrationWarning>
{quote} {/* ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ๋ค๋ฅผ ์ ์์ */}
</div>
);
}
disableHydrate ์ต์
Hydration์ ๋นํ์ฑํํ๊ณ CSR๋ก ์ ํ
// api/src/application/sonamu.ts
registerSSR({
path: "/admin/realtime-dashboard",
preload: (params) => [
/* ... */
],
disableHydrate: true, // Hydration ๋นํ์ฑํ
});
์๋ ๋ฐฉ์:
// entry-client.tsx
const ssrConfig = window.__SONAMU_SSR_CONFIG__;
if (ssrConfig?.disableHydrate) {
// Hydration ๋์ ์๋ก ๋ ๋๋ง
console.log("[Sonamu] Hydration disabled, rendering as CSR");
ReactDOM.createRoot(document).render(<RouterProvider router={router} />);
} else {
// ์ ์ Hydration
ReactDOM.hydrateRoot(document, <RouterProvider router={router} />);
}
์ฌ์ฉ ์ฌ๋ก:
- ์ค์๊ฐ ๋ฐ์ดํฐ๊ฐ ์ค์ํ ๊ฒฝ์ฐ (๋์๋ณด๋, ์ฑํ
๋ฑ)
- ์๋ฒ/ํด๋ผ์ด์ธํธ ๋ ๋๋ง ๊ฒฐ๊ณผ๊ฐ ์๋์ ์ผ๋ก ๋ค๋ฅธ ๊ฒฝ์ฐ
- Hydration mismatch ํด๊ฒฐ์ด ์ด๋ ค์ด ๊ฒฝ์ฐ
์ ์ฒด ๋ฌธ์ Hydration
Sonamu๋ document ์ ์ฒด๋ฅผ hydrateํฉ๋๋ค (div#root๊ฐ ์๋).
// โ ์ผ๋ฐ์ ์ธ React ์ฑ (div#root๋ง)
ReactDOM.hydrateRoot(
document.getElementById("root"),
<App />
);
// โ
Sonamu (document ์ ์ฒด)
ReactDOM.hydrateRoot(
document,
<RouterProvider router={router} />
);
์ด์ :
- TanStack Router๊ฐ
<html>, <head> ํ๊ทธ๊น์ง ๊ด๋ฆฌ
<HeadContent />, <Scripts /> ์ปดํฌ๋ํธ๊ฐ head/body ์์
- SEO ๋ฉํ ํ๊ทธ๋ฅผ ๋์ ์ผ๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅ
// web/src/routes/__root.tsx
export const Route = createRootRouteWithContext<RouterContext>()({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ title: "My App" },
],
}),
component: RootComponent,
});
function RootComponent() {
return (
<html lang="ko">
<head>
<HeadContent /> {/* head ํ๊ทธ ๋์ ์์ฑ */}
</head>
<body>
<div id="root">
<Outlet />
</div>
<Scripts /> {/* script ํ๊ทธ ๋์ ์์ฑ */}
</body>
</html>
);
}
Suspense์ Hydration
์๋ฒ์์ Suspense fallback์ ๋ ๋๋งํ๊ณ ํด๋ผ์ด์ธํธ์์ ์ค์ ์ปจํ
์ธ ๋ฅผ ๋ก๋ํ ์ ์์ต๋๋ค.
// entry-server.generated.tsx
const appHtml = renderToString(
<Suspense fallback={null}> {/* Hydration mismatch ๋ฐฉ์ง */}
<RouterProvider router={router} />
</Suspense>,
);
fallback=์ ์ฌ์ฉํ๋ ์ด์ :
- SSR์์๋ ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์ด๋ฏธ ๋ก๋๋์ด ์์
- fallback์ด ๋ณด์ด์ง ์์
- Hydration mismatch ๋ฐฉ์ง
QueryClient ์ค์
์๋ฒ ์ค์
// entry-server.generated.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000, // 5์ด
retry: false, // ์๋ฒ์์๋ ์ฌ์๋ ์ ํจ
},
},
});
ํด๋ผ์ด์ธํธ ์ค์
// entry-client.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000, // 5์ด
retry: false, // ์ฌ์๋ ์ ํจ
refetchOnMount: true, // ๋ง์ดํธ ์ ์ฌ๊ฒ์ฆ
},
},
});
์ฃผ์: ์๋ฒ์ ํด๋ผ์ด์ธํธ์ staleTime์ ๋์ผํ๊ฒ ์ค์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
Router Context
QueryClient๋ฅผ Router Context๋ก ์ ๋ฌํฉ๋๋ค.
// web/src/routes/__root.tsx
export interface RouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
});
function RootComponent() {
const { queryClient } = Route.useRouteContext();
return (
<html>
<body>
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
</body>
</html>
);
}
// entry-server.generated.tsx & entry-client.tsx
const router = createRouter({
routeTree,
context: { queryClient }, // Context๋ก ์ ๋ฌ
defaultPreload: "intent",
});
๋๋ฒ๊น
Hydration ํ์ธ
// entry-client.tsx
if (document.documentElement.innerHTML && dehydratedState) {
console.log("[Sonamu] Hydrating with SSR data");
console.log("Queries:", Object.keys(dehydratedState.queries || {}));
ReactDOM.hydrateRoot(document, <RouterProvider router={router} />);
} else {
console.log("[Sonamu] Rendering as CSR (no SSR data)");
ReactDOM.createRoot(document).render(<RouterProvider router={router} />);
}
QueryClient ์ํ ํ์ธ
// Chrome Extension ์ง์
window.__TANSTACK_QUERY_CLIENT__ = queryClient;
// ์ฝ์์์ ํ์ธ
window.__TANSTACK_QUERY_CLIENT__.getQueryCache().getAll();
์ฃผ์์ฌํญ
Hydration ์ฌ์ฉ ์ ์ฃผ์์ฌํญ: 1. ์๋ฒ/ํด๋ผ์ด์ธํธ HTML ์ผ์น: Hydration mismatch ๋ฐฉ์ง 2.
window ๊ฐ์ฒด ์ฃผ์: ์๋ฒ์์๋ window๊ฐ ์์ต๋๋ค 3. Date ๊ฐ์ฒด: dateReviver๊ฐ ์๋ ์ฒ๋ฆฌํ์ง๋ง
ํ์ธ ํ์ 4. QueryClient ์ค์ ๋์ผ: ์๋ฒ์ ํด๋ผ์ด์ธํธ์ staleTime ์ผ์น 5. disableHydrate
์ต์ํ: ํน๋ณํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉ
๋ค์ ๋จ๊ณ
SSR ์ค์
SSR ๊ธฐ๋ณธ ๊ตฌ์กฐ
๋ฐ์ดํฐ ํ๋ฆฌ๋ก๋ฉ
registerSSR ์ฌ์ฉ๋ฒ
์บ์ ์ ์ด
TanStack Query ์บ์ฑ
TanStack Query ๊ณต์ ๋ฌธ์
Query ์์ธ ๊ฐ์ด๋