๋ค์ด๊ฐ๋ฉฐ
์๋น์ค ์๊ฐ ํ์ด์ง๋ฅผ ๋ง๋ค ๋ ๊ณ ๋ฏผ์ด ์๊ฒผ์ต๋๋ค. ๋ฉ์ธ ํ์ด์ง์ ํ์ฌ ์๊ฐ ๊ฐ์ ์ ์ ํ์ด์ง๋ SEO๋ ์ค์ํ๊ณ ๋น ๋ฅธ ๋ก๋ฉ์ด ํ์ํ๋ฐ, ๋๋จธ์ง ํ์ด์ง๋ค์ ๊ทธ๋ฅ SPA๋ก ๋์ํด๋ ๊ด์ฐฎ์๊ฑฐ๋ ์. ํ์ง๋ง ์๋ฒ๋ฅผ ์ด์ํ๊ธฐ์ ๋น์ฉ ๋ถ๋ด์ด ์์๊ณ , ์ ์ ํธ์คํ ๋ง์ผ๋ก ํด๊ฒฐํ๊ณ ์ถ์์ต๋๋ค.
๊ฒฐ๋ก ๋ถํฐ ๋งํ๋ฉด, React Router 7์ ssr: false + prerender ์กฐํฉ์ผ๋ก ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค. ์ผ๋ถ ํ์ด์ง๋ ๋น๋ ํ์์ ๋ฏธ๋ฆฌ ๋ ๋๋ง(SSG)ํ๊ณ , ๋๋จธ์ง๋ SPA๋ก ๋์ํ๋ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์
๋๋ค.
์ด ๊ธ์์๋ ์ ๊ฐ ๊ฒฝํํ ์ํ์ฐฉ์ค์ ํด๊ฒฐ ๊ณผ์ , ๊ทธ๋ฆฌ๊ณ ์ถ๊ฐ๋ก ๋ฐ๊ฒฌํ SEO ํ๊น์ง ๊ณต์ ํ๊ฒ ์ต๋๋ค.
๋ชฉํ: ์๋ฒ ์๋ ์ ์ ํธ์คํ
์ ๊ฐ ์ํ๋ ๊ฒ
/,/about๊ฐ์ ์ฃผ์ ํ์ด์ง๋ SSG๋ก ๋น๋ ํ์์ HTML ์์ฑ (SEO๋ฅผ ์ํด)- ๋๋จธ์ง ํ์ด์ง๋ SPA๋ก ๋์
- ์๋ฒ ์์ด ์ ์ ํ์ผ๋ง์ผ๋ก ์ด์ (๋น์ฉ ์ ๊ฐ์ ์ํด)
React Router 7์ ์ด๋ฐ ์๊ตฌ์ฌํญ์ ์ ํํ ์ถฉ์กฑ์ํฌ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์ค์ : ssr: false + prerender
๊ฐ์ฅ ํต์ฌ์ด ๋๋ ์ค์ ์ react-router.config.ts์
๋๋ค:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
// SSR ๋นํ์ฑํ = ์๋ฒ ํ์ ์์
ssr: false,
// ํน์ ๊ฒฝ๋ก๋ง ๋ฏธ๋ฆฌ ๋ ๋๋ง
prerender: ["/", "/about"],
} satisfies Config;
์ด ์ค์ ์ด ์๋ฏธํ๋ ๊ฒ:
ssr: false: ๋ฐํ์ ์๋ฒ ๋ ๋๋ง์ ๋นํ์ฑํํฉ๋๋ค. ๋์ ๋ ๋๋ง์ ์ํ ์๋ฒ๊ฐ ํ์ ์์ต๋๋ค.prerender: ["/", "/about"]: ๋น๋ ํ์์ ์ด ๊ฒฝ๋ก๋ค์ ๋ฏธ๋ฆฌ HTML๋ก ๋ ๋๋งํฉ๋๋ค.
๋ฌธ์ ๋ฐ์: ๋น ํ์ด์ง๊ฐ ๋ ๋๋ง๋๋ค
์ฒ์ ์ค์ ํ๊ณ ๋น๋ํ์ ๋, ์ด์ํ ํ์์ด ๋ฐ์ํ์ต๋๋ค.
npm run build
๋น๋๋ ์ฑ๊ณตํ๊ณ , build/client/index.html๊ณผ build/client/about/index.html ํ์ผ๋ ์์ฑ๋์ต๋๋ค. ํ์ง๋ง ํ์ผ์ ์ด์ด๋ณด๋ ๋ด์ฉ์ด ๋น์ด์์์ต๋๋ค. HydrateFallback์ โLoadingโฆโ ๋ฉ์์ง๋ง ์๊ณ , ์ค์ ์ปจํ
์ธ ๋ ์์์ด์.
<!-- ์์ฑ๋ index.html -->
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div id="root">Loading...</div>
<script src="/assets/entry.client.js"></script>
</body>
</html>
์ ๊ฐ ์๋ ์ ์ ๋ ๋๋ง(SSG)์ ์ด๋ฐ๊ฒ ์๋๋ฐ ๋ง์ด์ฃ โฆ HeroSection, ์๊ฐ ๋ฌธ๊ตฌ, ์ด๋ฏธ์งโฆ ๋ชจ๋ ์ปจํ ์ธ ๊ฐ ๋น ์ ธ์์์ต๋๋ค.
์์ธ: loader๊ฐ ์์ผ๋ฉด prerender๋ฅผ ๊ฑด๋๋ด๋ค
๊ณต์ ๋ฌธ์์ AI ๋ํ ํํ์ ํตํด ์์ธ์ ์ฐพ์์ต๋๋ค. React Router 7์ prerendering์ loader์ ์กด์ฌ ์ฌ๋ถ๋ก ํ์ด์ง๋ฅผ ๋ ๋๋งํ ์ง ๊ฒฐ์ ํฉ๋๋ค.
loader๊ฐ ์๋ค๋ฉด?
// app/pages/home/index.tsx
export default function HomePage() {
return (
<div>
<h1>Welcome!</h1>
<p>This is our homepage</p>
</div>
);
}
์ด ์ฝ๋๋ก ๋น๋ํ๋ฉด React Router๋ ์ด๋ ๊ฒ ํ๋จํฉ๋๋ค.
์ด ํ์ด์ง๋ ์๋ฒ(๋น๋ ํ์)์์ ๊ฐ์ ธ์ฌ ๋ฐ์ดํฐ๊ฐ ์๋ค? ๊ทธ๋ผ ๊ตณ์ด ๋ฏธ๋ฆฌ ๋ ๋๋งํ์ง ์๊ณ , ๋ธ๋ผ์ฐ์ ์์ JavaScript๊ฐ ์คํ๋ ๋ ๋ ๋๋งํ๋ฉด ๋๊ฒ ๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ๋น๋ ์์ ์๋ HydrateFallback๋ง ๋ ๋๋ง๋๊ณ , ์ค์ ์ปจํ
์ธ ๋ ๋น๋ ๊ฒฐ๊ณผ๋ฌผ html์ ํฌํจ๋์ง ์์๋ ๊ฒ์
๋๋ค.
loader๋ฅผ ์ถ๊ฐํ๋ฉด?
// app/pages/home/index.tsx
// ๋น loader๋ผ๋ ์ถ๊ฐ
export function loader() {}
export default function HomePage() {
return (
<div>
<h1>Welcome!</h1>
<p>This is our homepage</p>
</div>
);
}
์ด์ React Router๋ ๋ค๋ฅด๊ฒ ๋ฐ์ํฉ๋๋ค.
loader๊ฐ ์๋ค? ์ด ํ์ด์ง๋ ์๋ฒ(๋น๋ ํ์)์์ ์คํ๋์ด์ผ ํ๋ ๋ก์ง์ด ์๊ตฌ๋. ๋น๋ ์์ ์ ๋ ๋๋งํด์ HTML์ ํฌํจ์ํค์.
๋น๋ ๊ฒฐ๊ณผ:
<!-- ์์ฑ๋ index.html -->
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div id="root">
<div>
<h1>Welcome!</h1>
<p>This is our homepage</p>
</div>
</div>
<script src="/assets/entry.client.js"></script>
</body>
</html>
๋๋์ด ์ค์ ์ปจํ ์ธ ๊ฐ HTML์ ํฌํจ๋ฉ๋๋ค. ํด..
ํด๊ฒฐ์ฑ ์ ๋น loader๋ผ๋ ์ถ๊ฐํด์ฃผ๊ธฐ
prerenderํ๊ณ ์ถ์ ๋ชจ๋ ํ์ด์ง์ loader๋ฅผ ์ถ๊ฐํ์ต๋๋ค. ๋ฐ์ดํฐ๊ฐ ํ์ ์์ด๋ ๋น ํจ์๋ก๋ผ๋ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
// app/pages/home/index.tsx
export function loader() {}
export default function HomePage() {
return (
<div>
<HeroSection />
<Features />
<CallToAction />
</div>
);
}
// app/pages/about/index.tsx
export function loader() {}
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<CompanyHistory />
<TeamMembers />
</div>
);
}
์ด์ ๋น๋ํ๋ฉด
> react-router build
Prerender: Generated build/client/index.html
Prerender: Generated build/client/index.data
Prerender: Generated build/client/about/index.html
Prerender: Generated build/client/about/index.data
Prerender: Generated build/client/__spa-fallback.html
์๋ฒฝํฉ๋๋ค! ๊ฐ ํ์ด์ง๋ง๋ค:
index.html: ์ด๊ธฐ ๋ฌธ์ ์์ฒญ์ ์ํ ์์ ํ HTMLindex.data: ํด๋ผ์ด์ธํธ ๋ค๋น๊ฒ์ด์ ์ ์ํ ๋ฐ์ดํฐ ํ์ผ__spa-fallback.html: prerender๋์ง ์์ ๊ฒฝ๋ก๋ฅผ ์ํ SPA fallback (403, 404 ์๋ฌ fallback๋ก ์ฌ์ฉ)
์ต์ข ์ ์ผ๋ก ๊ธฐ๋๋๋ ๋์ ์ ๋ฆฌ
๋น๋ ํ์
- React Router๋
prerender๋ฐฐ์ด์ ๊ฐ ๊ฒฝ๋ก๋ฅผ ํ์ธํฉ๋๋ค - ๊ฐ ๊ฒฝ๋ก์ ๋งค์นญ๋๋ route๋ค์ ์ฐพ์ต๋๋ค
- loader๊ฐ ์๋ route๋ค์ ๋น๋ ์์ ์ ์คํํฉ๋๋ค
- ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ์ฌ HTML์ ์์ฑํฉ๋๋ค
- HTML๊ณผ ๋ฐ์ดํฐ ํ์ผ์
build/client์ ์ ์ฅํฉ๋๋ค
๋ฐํ์ (๋ธ๋ผ์ฐ์ )
prerender๋ ํ์ด์ง ์ ๊ทผ ์ (/, /about):
- ์๋ฒ(์ ์ ํธ์คํ
)๊ฐ
index.html์ ๋ฐํ - HTML์ ์ด๋ฏธ ์ปจํ ์ธ ๊ฐ ํฌํจ๋์ด ์์ด ์ฆ์ ํ์๋จ
- JavaScript๊ฐ ๋ก๋๋๋ฉด hydration ์งํ
- ์ดํ ํด๋ผ์ด์ธํธ ๋ผ์ฐํ ์ผ๋ก ๋์
prerender๋์ง ์์ ํ์ด์ง ์ ๊ทผ ์ (/contact, /pricing ๋ฑ):
- ์๋ฒ๊ฐ 404, 403 ๋ฑ์ ๋ฐํํ์ง๋ง
__spa-fallback.html๋ก ๋ฆฌ๋ค์ด๋ ํธ - SPA fallback HTML์ด ๋ก๋๋จ (๊ธฐ๋ณธ shell๋ง ํฌํจ)
- JavaScript๊ฐ ์คํ๋๊ณ ๋ธ๋ผ์ฐ์ ์์ ํ์ด์ง ๋ ๋๋ง
- SPA๋ก ๋์
์ ์ ํธ์คํ ์ค์ : 404 ์ฒ๋ฆฌ
prerender๋์ง ์์ ๊ฒฝ๋ก์ ๋ํ 404 ์ฒ๋ฆฌ๊ฐ ์ค์ํฉ๋๋ค. ๋๋ถ๋ถ์ ์ ์ ํธ์คํ ์๋น์ค๋ ์ค์ ํ์ผ๋ก ์ด๋ฅผ ์ง์ํฉ๋๋ค.
AWS S3 + CloudFront ์กฐํฉ์ด๋ผ๋ฉด ์ค์ ์ ๋ฐ๋ผ 403 ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
Netlify (_redirects)
# / ๊ฒฝ๋ก๋ฅผ prerenderํ์ง ์์ ๊ฒฝ์ฐ
/* /index.html 200
# / ๊ฒฝ๋ก๋ฅผ prerenderํ ๊ฒฝ์ฐ
/* /__spa-fallback.html 200
Vercel (vercel.json)
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/__spa-fallback.html"
}
]
}
Cloudflare Pages (_redirects)
/* /__spa-fallback.html 200
์ด ์ค์ ์ด ์์ผ๋ฉด /contact ๊ฐ์ ๊ฒฝ๋ก์ ์ง์ ์ ๊ทผํ์ ๋ 404 ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
SEO ๋ณด๋์ค: Canonical Tag๋ก ์ค๋ณต URL ํตํฉํ๊ธฐ
์ ์ ์ฌ์ดํธ๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด /about๊ณผ /about/ ๊ฐ์ trailing slash ๋ฌธ์ ๋ฅผ ๋ง๋ฉ๋๋ค. ์ด ๋์ ๊ธฐ์ ์ ์ผ๋ก ๋ค๋ฅธ URL์ด์ง๋ง, ๊ฐ์ ํ์ด์ง๋ฅผ ๊ฐ๋ฆฌํต๋๋ค.
๋ฌธ์ ๋ ๋ถ์ ๋๊ตฌ ์๋น์ค(GA, Mixpanel ๋ฑ)์ ๋ฐ๋ผ ์ด ๋์ ๋ณ๊ฐ๋ก ์ง๊ณํ ๊ฐ๋ฅ์ฑ์ด ์๋ค๋ ์ ์ ๋๋ค. ๋ฐ์ดํฐ๊ฐ ๋ถ์ฐ๋๊ณ , SEO ์ธก๋ฉด์์๋ ์ข์ง ์์ฃ .
ํด๊ฒฐ์ฑ : Canonical Tag
1. React 19์ ๋ด์ฅ <link> ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํด์ canonical URL์ ๋ช
์
// app/pages/about/index.tsx
export function loader() {}
export default function AboutPage() {
return (
<div>
{/* Canonical tag๋ก ์ ๊ท URL ๋ช
์ */}
<link rel="canonical" href="https://yourdomain.com/about" />
<h1>About Us</h1>
<p>We are a great company...</p>
</div>
);
}
2. ๊ธ๋ก๋ฒ ์ ์ฉ
// app/root.tsx
export function Layout({ children }: { children: React.ReactNode }) {
const canonicalUrl = useCanonical();
...
}
// app/shared/hooks/use-canonical.tsx
import { useLocation } from "react-router";
export const useCanonical = () => {
const location = useLocation();
const origin = import.meta.env.VITE_SITE_URL;
const canonicalPath = location.pathname.replace(/\/+$/, "");
const canonicalUrl = `${origin}${canonicalPath}`;
return canonicalUrl;
};
์ด์ ์ฌ์ฉ์๊ฐ /about ๋๋ /about/ ์ด๋ ์ชฝ์ผ๋ก ์ ๊ทผํ๋๋ผ๋, ๊ฒ์ ์์ง๊ณผ ๋ถ์ ํด์ https://yourdomain.com/about์ ์ ๊ท URL๋ก ์ธ์ํฉ๋๋ค.
์ฅ์ ๊ณผ ์ฃผ์์ฌํญ
์ฅ์
- ๋น์ฉ ์ ๊ฐ: ์๋ฒ ์์ด ์ ์ ํธ์คํ ๋ง์ผ๋ก ์ด์ (Cloudflare Pages, Netlify, AWS S3 + CloudFront ๋ฑ ํ์ฉ)
- ๋น ๋ฅธ ๋ก๋ฉ: ์ฃผ์ ํ์ด์ง๋ ๋ฏธ๋ฆฌ ๋ ๋๋ง๋์ด ์์ด ์ฆ์ ํ์
- SEO ์ต์ ํ: prerender๋ ํ์ด์ง๋ ๊ฒ์ ์์ง์ด ์์ ํ HTML์ ํฌ๋กค๋ง ๊ฐ๋ฅ
- ์ ์ฐ์ฑ: ํ์ํ ํ์ด์ง๋ง ์ ํ์ ์ผ๋ก SSG ์ ์ฉ
- ๊ฐ๋จํ ๋ฐฐํฌ:
build/clientํด๋๋ง ์ ๋ก๋ํ๋ฉด ๋
์ฃผ์์ฌํญ
- loader ํ์: prerenderํ๊ณ ์ถ์ ๋ชจ๋ ํ์ด์ง์ loader ์ถ๊ฐ (๋น ํจ์๋ผ๋)
- ๋น๋ ํ์ ์ฆ๊ฐ: prerenderํ ํ์ด์ง๊ฐ ๋ง์์๋ก ๋น๋ ์๊ฐ ์ฆ๊ฐ
- ๋์ ๋ฐ์ดํฐ ์ ํ: ๋น๋ ํ์์ ๊ฒฐ์ ๋๋ฏ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ๋ ํด๋ผ์ด์ธํธ์์ fetch ํ์
- 404 ์ค์ ํ์: ํธ์คํ ์๋น์ค์ fallback ์ค์ ๋ฐ๋์ ํ์
- ์๋ฒ ํจ์ ํ์ฉ ๊ธ์ง:
ssr: false์ผ ๋๋ prerender๊ฐ ์๋ ํ์ด์ง์์ ์๋ฒ ํจ์ ์ฌ์ฉ ๋ถ๊ฐ
์ธ์ ์ด ํจํด์ ์ฌ์ฉํ ๊น?
์ด ํจํด์ด ์ ํฉํ ๊ฒฝ์ฐ
- ๋ง์ผํ ์ฌ์ดํธ + ์น ์ฑ ํ์ด๋ธ๋ฆฌ๋
- ์ผ๋ถ ํ์ด์ง๋ง SEO๊ฐ ์ค์ํ ๊ฒฝ์ฐ
- ์๋ฒ ์ด์์ ์ํ๊ณ ์ถ์ ๊ฒฝ์ฐ
- ์ ์ ํธ์คํ ์ ์ฅ์ (CDN, ์บ์ฑ ๋ฑ)์ ํ์ฉํ๊ณ ์ถ์ ๊ฒฝ์ฐ
๋์์ ๊ณ ๋ คํด์ผ ํ๋ ๊ฒฝ์ฐ
- ๋ชจ๋ ํ์ด์ง๊ฐ ์ค์๊ฐ ๋ฐ์ดํฐ์ ์์กดํ๋ ๊ฒฝ์ฐ
- ํ์ด์ง๊ฐ ์๋ฐฑ~์์ฒ ๊ฐ์ธ ๊ฒฝ์ฐ (๋น๋ ์๊ฐ ๋ฌธ์ )
- ์ฌ์ฉ์ ์ธ์ฆ์ด ํ์ํ ํ์ด์ง๊ฐ ๋๋ถ๋ถ์ธ ๊ฒฝ์ฐ
๋ง์น๋ฉฐ
React Router 7์ ssr: false + prerender ์กฐํฉ์ ๊ฐ๋ ฅํ ํจํด์
๋๋ค. ์๋ฒ ์์ด๋ SEO์ ์ฑ๋ฅ์ ๋ชจ๋ ์ก์ ์ ์๊ณ , ํ์ํ ๊ณณ์๋ง SSG๋ฅผ ์ ์ฉํ ์ ์๋ ์ ์ฐ์ฑ์ ์ ๊ณตํฉ๋๋ค.
์ฒ์์๋ โ์ prerender๊ฐ ์ ๋์ง?โ๋ผ๋ฉฐ ์ฝ์งํ์ง๋ง, loader์ ์ญํ ์ ์ดํดํ๊ณ ๋๋ ๋ชจ๋ ๊ฒ ๋ช ํํด์ก์ต๋๋ค. ๋น loader ํ๋๋ก ๋น๋ ์์คํ ์๊ฒ โ์ด ํ์ด์ง๋ ๋ฏธ๋ฆฌ ๋ ๋๋งํด์คโ ๋ผ๊ณ ์ ํธ๋ฅผ ๋ณด๋ด๋ ๊ฒ์ด์ฃ .
์ฌ๊ธฐ์ canonical tag๊น์ง ์ถ๊ฐํ๋ฉด SEO์ ๋ถ์ ๋ฐ์ดํฐ๊น์ง ๊น๋ํ๊ฒ ์ ๋ฆฌํ ์ ์์ต๋๋ค.
์ด ํจํด์ ํ์ฉํด๋ณด์ธ์. ํนํ ์๋น์ค ์๊ฐ ํ์ด์ง๋ ๋ง์ผํ ์ฌ์ดํธ๋ฅผ ๋ง๋ค ๋ ์ ์ฉํฉ๋๋ค