๐ SEO ์นํ์ ์ด์ง ์์ ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง(CSR) ๋จ์ฉ: ๊ฒ์ ์์ง์ ์ธ๋ฉด
Summary
ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง(CSR)๋ง์ผ๋ก ์น์ฌ์ดํธ๋ฅผ ๊ตฌ์ถํ ๊ฒฝ์ฐ, ๊ฒ์ ์์ง ํฌ๋กค๋ฌ๊ฐ JavaScript๋ฅผ ์์ ํ ์คํํ์ง ๋ชปํ๊ฑฐ๋ ๋๋ฆฌ๊ฒ ์ฒ๋ฆฌํ์ฌ SEO์ ์น๋ช ์ ์ธ ์ํฅ์ ์ค๋๋ค. SEO์ ์ค์ํ ํ์ด์ง์๋ SSR, SSG ๋๋ ํ์ด๋ธ๋ฆฌ๋ ๋ ๋๋ง ์ ๋ต์ ์ฌ์ฉํ์ฌ ๊ฒ์ ์์ง ์นํ์ ์ธ ์ฝํ ์ธ ๋ฅผ ์ ๊ณตํ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํด์ผ ํฉ๋๋ค.
Why Wrong?
ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ๋ ๋๋ง(CSR)์ ์ด๊ธฐ ๋ก๋ฉ ์ ๋น HTML์ ์ ๊ณตํ๊ณ JavaScript๊ฐ ์คํ๋ ํ์์ผ ์ฝํ ์ธ ๋ฅผ ๋์ ์ผ๋ก ์์ฑํฉ๋๋ค. ์ด ๋ฐฉ์์ ๋ชจ๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฐ์ฑ์ ์ ๊ณตํ์ง๋ง, ๊ฒ์ ์์ง ์ต์ ํ(SEO) ์ธก๋ฉด์์๋ ์น๋ช ์ ์ธ ์ฝ์ ์ ๊ฐ์ง๋๋ค. ํนํ Google ์ธ์ ๋ค๋ฅธ ๊ฒ์ ์์ง ํฌ๋กค๋ฌ๋ค์ JavaScript๋ฅผ ์๋ฒฝํ๊ฒ ์คํํ๊ณ ํ์ฑํ๋ ๋ฐ ํ๊ณ๊ฐ ์๊ฑฐ๋, ์ฒ๋ฆฌ ์๋๊ฐ ๋งค์ฐ ๋๋ฆด ์ ์์ต๋๋ค. ์ด๋ ํฌ๋กค๋ฌ๊ฐ ํ์ด์ง์ ์ค์ ์ฝํ ์ธ ๋ฅผ ์ ๋๋ก ์ธ๋ฑ์ฑํ์ง ๋ชปํ๊ฒ ํ์ฌ, ๊ฒ์ ๊ฒฐ๊ณผ์ ๋ ธ์ถ๋์ง ์๊ฑฐ๋ ๋ฎ์ ์์๋ฅผ ๊ธฐ๋กํ๊ฒ ๋ง๋ญ๋๋ค. ๋ํ, ์ฌ์ฉ์๊ฐ ๋น ํ๋ฉด์ ๋ณด๋ ์๊ฐ์ด ๊ธธ์ด์ ธ Core Web Vitals(LCP, FID) ์ ์๊ฐ ๋ฎ์์ง๊ณ ์ฌ์ฉ์ ๊ฒฝํ(UX)์๋ ๋ถ์ ์ ์ธ ์ํฅ์ ๋ฏธ์นฉ๋๋ค. ์์ ๋ฏธ๋์ด ํ๋ซํผ ๋ํ ์๋ฒ์์ ์ ๊ณต๋๋ HTML ์ฝํ ์ธ ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์์ฑํ๊ธฐ ๋๋ฌธ์, CSR ๋ฐฉ์๋ง์ผ๋ก๋ ์ ์ ํ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ ๋ํ๋์ง ์์ ์ ์์ต๋๋ค.
How to Fix?
SEO์ ๋ฏผ๊ฐํ ํ์ด์ง๋ ์ด๊ธฐ ๋ก๋ฉ ์๋๊ฐ ์ค์ํ ํ์ด์ง์ ๊ฒฝ์ฐ, ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR)์ด๋ ์ ์ ์ฌ์ดํธ ์์ฑ(SSG)๊ณผ ๊ฐ์ ๋ ๋๋ง ์ ๋ต์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. SSR์ ์๋ฒ์์ ๋ฏธ๋ฆฌ HTML์ ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ์ ๋ณด๋ด์ฃผ๋ฏ๋ก, ํฌ๋กค๋ฌ๊ฐ ์ฆ์ ์ฝํ ์ธ ๋ฅผ ํ์ฑํ ์ ์๊ฒ ํ๊ณ ์ฌ์ฉ์์๊ฒ๋ ๋น ๋ฅธ ์ด๊ธฐ ํ๋ฉด์ ์ ๊ณตํฉ๋๋ค. SSG๋ ๋น๋ ์์ ์ ๋ชจ๋ ํ์ด์ง๋ฅผ HTML ํ์ผ๋ก ์์ฑํ์ฌ CDN์ ๋ฐฐํฌํจ์ผ๋ก์จ, ๋งค์ฐ ๋น ๋ฅธ ์๋ต ์๋์ ๋ฐ์ด๋ SEO ํจ์จ์ ๋ณด์ฅํฉ๋๋ค. React ๊ธฐ๋ฐ์ Next.js๋ Vue ๊ธฐ๋ฐ์ Nuxt.js์ ๊ฐ์ ํ๋ ์์ํฌ๋ SSR/SSG ๊ธฐ๋ฅ์ ์ฝ๊ฒ ํตํฉํ ์ ์๋๋ก ์ง์ํ๋ฉฐ, 'ํ์ด๋๋ ์ด์ (Hydration)'์ ํตํด ์๋ฒ์์ ์์ฑ๋ HTML์ ํด๋ผ์ด์ธํธ ์ฌ์ด๋์์ ์ธํฐ๋ํฐ๋ธํ๊ฒ ๋ง๋ญ๋๋ค. ์ด๋ฅผ ํตํด ๊ฒ์ ์์ง ์นํ์ ์ด๋ฉด์๋ ํ๋ถํ ์ฌ์ฉ์ ๊ฒฝํ์ ๋์์ ์ ๊ณตํ ์ ์์ต๋๋ค.
Before Code (Bad)
// src/pages/ProductList.jsx
import React, { useState, useEffect } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProducts = async () => {
try {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setProducts(data);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []); // ๋น ์์กด์ฑ ๋ฐฐ์ด๋ก ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ํ ๋ฒ๋ง ์คํ
if (loading) return <div>์ํ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ ์ค...</div>;
if (error) return <div>์๋ฌ ๋ฐ์: {error.message}</div>;
return (
<div>
<h1>๐๏ธ ์ธ๊ธฐ ์ํ ๋ชฉ๋ก</h1>
<ul>
{products.map(product => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>๊ฐ๊ฒฉ: {product.price}์</span>
</li>
))}
</ul>
</div>
);
}
export default ProductList;
// public/index.html (์ด ์์๋ CSR์ ์ผ๋ฐ์ ์ธ ์์์ ์์ ๋ณด์ฌ์ค)
// <div id="root"></div>
// <script src="/src/index.js"></script> // JavaScript๊ฐ ๋ก๋๋๊ณ ์คํ๋์ด์ผ ์ฝํ
์ธ ๊ฐ ๋ณด์
After Code (Good)
// pages/products/index.js (Next.js๋ฅผ ํ์ฉํ SSR/SSG ์์)
import React from 'react';
function ProductList({ products }) {
// products๋ ์ด๋ฏธ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ props๋ก ์ ๋ฌ๋ ์ํ
return (
<div>
<h1>๐๏ธ ์ธ๊ธฐ ์ํ ๋ชฉ๋ก</h1>
<ul>
{products.map(product => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>๊ฐ๊ฒฉ: {product.price}์</span>
</li>
))}
</ul>
</div>
);
}
// SSR (Server-Side Rendering)
// ์์ฒญ ์๋ง๋ค ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ํ์ด์ง๋ฅผ ๋ฏธ๋ฆฌ ๋ ๋๋ง
export async function getServerSideProps() {
// ์ด ์ฝ๋๋ ์๋ฒ์์ ์คํ๋ฉ๋๋ค.
const response = await fetch('http://localhost:3000/api/products'); // ์ค์ API ์๋ํฌ์ธํธ
const products = await response.json();
return {
props: {
products, // props๋ก ์ปดํฌ๋ํธ์ ์ ๋ฌ
},
};
}
// ๋๋ SSG (Static Site Generation)
// ๋น๋ ์์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ์ ์ ์ธ HTML ํ์ผ ์์ฑ (์์ฃผ ๋ณํ์ง ์๋ ๋ฐ์ดํฐ์ ์ ํฉ)
/*
export async function getStaticProps() {
const response = await fetch('http://localhost:3000/api/products');
const products = await response.json();
return {
props: {
products,
},
revalidate: 60, // 60์ด๋ง๋ค ISR(Incremental Static Regeneration) ์๋
};
}
*/
export default ProductList;
// ๊ฒฐ๊ณผ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ๋ JS๊ฐ ์คํ๋๊ธฐ ์ ๋ถํฐ ์์ ํ HTML ์ฝํ
์ธ ๋ฅผ ๋ฐ๊ฒ ๋ฉ๋๋ค.
/*
<!-- ์๋ฒ์์ ๋ ๋๋ง๋ HTML ์์ (Next.js ๊ฒฐ๊ณผ) -->
<body>
<div id="__next">
<div>
<h1>๐๏ธ ์ธ๊ธฐ ์ํ ๋ชฉ๋ก</h1>
<ul>
<li>
<h2>์ํ 1</h2>
<p>์ํ 1์ ๋ํ ์ค๋ช
</p>
<span>๊ฐ๊ฒฉ: 10000์</span>
</li>
<li>
<h2>์ํ 2</h2>
<p>์ํ 2์ ๋ํ ์ค๋ช
</p>
<span>๊ฐ๊ฒฉ: 20000์</span>
</li>
</ul>
</div>
</div>
<!-- ์ดํ Next.js์ ํด๋ผ์ด์ธํธ ์ธก JS๊ฐ ๋ก๋๋์ด Hydration ์งํ -->
</body>
*/