๐ 01. SSR๊ณผ HTML: Next.js๊ฐ HTML์ ๋ง๋๋ ๋ฐฉ๋ฒ
๐ ๊ฐ์
CSR vs SSR vs SSG vs ISR โ ๊ฐ ๋ ๋๋ง ์ ๋ต์ด ์์ฑํ๋ HTML์ ์ฐจ์ด, Hydration์ด๋ ๋ฌด์์ธ๊ฐ, Next.js App Router์ ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ HTML ๊ด์ ์์ ํํค์นฉ๋๋ค.
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ / ํต์ฌ ํํธ๋ง: 12๋ถ
๐ฏ ์ด ๋ฌธ์์ ์์น
์ด ๋ฌธ์๋ HTML ์ฌํ ์น์ ์ ๋๋ค. HTML guide 01๋ฒ(HTML ๋ฉํ ๋ชจ๋ธ)๊ณผ Next.js ๋ ๋๋ง ์ ๋ต์ ๋ํ ๊ธฐ๋ณธ ์ดํด๊ฐ ์์ด์ผ ํฉ๋๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[CSR์ ํ๊ณ] โ [SSR์ด ๋ค๋ฅธ ์ด์ ] โ [Next.js๊ฐ HTML์ ๋ง๋๋ ๋ฐฉ๋ฒ] โ [Hydration] โ [์๋ฒ ์ปดํฌ๋ํธ์ HTML]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- CSR๊ณผ SSR์์ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ฐ๋ ์ด๊ธฐ HTML์ ์ฐจ์ด๋ฅผ ์ค๋ช ํ ์ ์๋ค.
- Hydration์ด ๋ฌด์์ด๋ฉฐ ์ ํ์ํ์ง ์๋ฆฌ๋ก ์ค๋ช ํ ์ ์๋ค.
- Next.js App Router์ ์๋ฒ ์ปดํฌ๋ํธ๊ฐ HTML ์์ฑ์ ์ด๋ป๊ฒ ๊ธฐ์ฌํ๋์ง ์ค๋ช ํ ์ ์๋ค.
- Hydration Mismatch ์๋ฌ๊ฐ ์ ๋ฐ์ํ๋์ง ์ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ๐ฃ ์์ฒ ( ์ ์ ): "์ํธ ๋, Next.js ์ฐ๋ฉด SEO๊ฐ ์ข๋ค๊ณ ํ๋๋ฐ, ๊ทธ๊ฒ HTML์ด๋ ์ด๋ค ๊ด๊ณ์์? React๋ JavaScript๋ก HTML์ ๋ง๋๋ ๊ฑฐ์์์. ๊ทธ๋ผ ๊ตฌ๊ธ ๋ด์ด ํ์ด์ง ๊ธ์ ๋ ๋ญ ๋ณด๋ ๊ฑด๊ฐ์?"
- ๐ฆ ์ํธ ( ๋ฆฌ๋ ): "์ ํํ ๊ฑฐ๊ธฐ์๋ถํฐ ์์ํด์ผ ํด์. ๊ตฌ๊ธ ๋ด์ด ์์๋ค ์ปค๋ฎค๋ํฐ์ ์ ์ํ๋ฉด, ์ฒซ ๋ฒ์งธ๋ก ๋ฐ๋ HTML ํ์ผ์ด ๋ญ๋๊ฐ SEO์ ํต์ฌ์ด๊ฑฐ๋ ์. CSR์ ๋น ๊ป๋ฐ๊ธฐ HTML์ ์ฃผ๊ณ , SSR์ ์ฝํ ์ธ ๊ฐ ์ฑ์์ง HTML์ ์ค์. ๊ทธ ์ฐจ์ด๊ฐ Google ๊ฒ์ ์์์ ์ง๊ฒฐ๋ผ์."
๐ค ์ ์์์ผ ํ๋๊ฐ
์์๋ค ์ปค๋ฎค๋ํฐ ์ถ์ ํ, ๊ฒ์๊ธ์ด ๊ตฌ๊ธ์ ์ ๊ฑธ๋ฆฌ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
๐ ์์(PM): "์์ฒ ๋, ์ฐ๋ฆฌ ๊ฒ์๊ธ์ ๊ตฌ๊ธ์์ ๊ฒ์ํ๋ฉด ์ ๋์์. SEO ์ค์ ๋ค ํ๋ค๊ณ ํ์ง ์์์ด์?"
์์ฒ ์ด์ ๊ธฐ์กด ์ ์ : ์์ React CRA(Create React App). ์ฆ CSR(Client-Side Rendering) ์ด์๋ค.
๐ 1. CSR vs SSR โ ๋ธ๋ผ์ฐ์ ๊ฐ ๋ฐ๋ HTML์ ์ฐจ์ด
CSR (Client-Side Rendering)
<!-- ๊ตฌ๊ธ ๋ด์ด CSR ์ฌ์ดํธ์ ์ ์ํด์ ๋ฐ๋ HTML -->
<!doctype html>
<html>
<head>
<title>์์๋ค ์ปค๋ฎค๋ํฐ</title>
</head>
<body>
<!-- ๋น ๊ป๋ฐ๊ธฐ! ์ฝํ
์ธ ๊ฐ ์์ -->
<div id="root"></div>
<!-- JS ๋ฒ๋ค์ด ์คํ๋์ด์ผ ์ด div ์์ ์ฝํ
์ธ ๊ฐ ์ฑ์์ง -->
<script src="/static/js/bundle.js"></script>
</body>
</html>๊ตฌ๊ธ ๋ด์ JavaScript๋ฅผ ์คํํ๊ธด ํ์ง๋ง, ์คํ์ ์๊ฐ ์ง์ฐ์ด ์๊ณ JS๊ฐ ๋ฌด๊ฑฐ์ธ์๋ก ํฌ๋กค๋ง ํ์ง์ด ๋จ์ด์ ธ. ์ฝํ ์ธ ๊ฐ ์๋ ์ด๊ธฐ HTML์ SEO์ ๋ถ๋ฆฌํด.
SSR (Server-Side Rendering)
<!-- ๊ตฌ๊ธ ๋ด์ด Next.js SSR ์ฌ์ดํธ์ ์ ์ํด์ ๋ฐ๋ HTML -->
<!doctype html>
<html lang="ko">
<head>
<title>๋ฆฌ์กํธ ์ํ๊ด๋ฆฌ ๋น๊ต | ์์๋ค ์ปค๋ฎค๋ํฐ</title>
<meta name="description" content="Zustand, Jotai ์ค๋ฌด ๋น๊ต ๋ถ์" />
</head>
<body>
<!-- ์ด๋ฏธ ์ฝํ
์ธ ๊ฐ ์ฑ์์ ธ ์์! -->
<main>
<article>
<h1>๋ฆฌ์กํธ ์ํ๊ด๋ฆฌ ๋น๊ต: Zustand vs Jotai</h1>
<p>์ ๋ ์์ฆ Zustand๋ฅผ ์ฐ๋๋ฐ, Jotai์ ์ฑ๋ฅ ์ฐจ์ด๊ฐ...</p>
<section>
<h2>๋๊ธ 5๊ฐ</h2>
<!-- ๋๊ธ๋ค๋ HTML์ ํฌํจ -->
</section>
</article>
</main>
<!-- JS๋ Hydration์ ์ํด ๋ก๋ -->
<script src="/_next/static/chunks/app.js" defer></script>
</body>
</html>์๋ฒ์์ ์ด๋ฏธ ๋ ๋๋ง๋ HTML์ ๋ธ๋ผ์ฐ์ ์ ์ ๋ฌํด. ๊ตฌ๊ธ ๋ด์ด JavaScript ์คํ ์ ์๋ ์ฝํ ์ธ ๋ฅผ ์ฝ์ ์ ์์ด.
๐ง 2. Hydration โ ์ ์ HTML์ ์ธํฐ๋ํฐ๋นํฐ ์ฌ๊ธฐ
SSR๋ก ๋ ๋๋ง๋ HTML์ ํ๋ฉด์ ๋ฐ๋ก ๋ณด์ด์ง๋ง, ์์ง ์ธํฐ๋ํฐ๋ธํ์ง ์์ (๋ฒํผ ํด๋ฆญ, ์ํ ๋ณํ ์์). Hydration์ ์ด ์ ์ HTML์ React์ ์ด๋ฒคํธ ์ฒ๋ฆฌ์ ์ํ ๊ด๋ฆฌ๋ฅผ ์ฐ๊ฒฐํ๋ ๊ณผ์ ์ด์ผ.
[์๋ฒ] โ HTML ์์ฑ โ ๋ธ๋ผ์ฐ์ ์ ์ ์ก
โ
[๋ธ๋ผ์ฐ์ ] HTML ์ฆ์ ํ์ (์ฌ์ฉ์๊ฐ ๋น ๋ฅด๊ฒ ๋ด)
โ
[๋ธ๋ผ์ฐ์ ] JS ๋ฒ๋ค ๋ค์ด๋ก๋ + ์คํ
โ
[React] ๊ธฐ์กด DOM์ ๋ค์ ๋ ๋๋งํ์ง ์๊ณ ,
์ด๋ฏธ ์๋ DOM์ ์ด๋ฒคํธ ํธ๋ค๋ฌ + ์ํ ์ฐ๊ฒฐ
= Hydration ์๋ฃ โ ์ธํฐ๋ํฐ๋ธํ ์ฑ์ด ๋จ
// Next.js Pages Router: ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ props๋ก ์ ๋ฌ
export async function getServerSideProps() {
const post = await fetchPost(params.id);
return { props: { post } };
}
export default function PostPage({ post }) {
const [liked, setLiked] = useState(false);
return (
<article>
<h1>{post.title}</h1>
{/* ์ด ๋ฒํผ์ Hydration ์ ์ ํด๋ฆญ ์ ๋จ */}
<button onClick={() => setLiked(!liked)}>
{liked ? "โค๏ธ" : "๐ค"} ์ข์์
</button>
</article>
);
}Hydration Mismatch ์๋ฌ:
์๋ฒ์์ ๋ ๋๋งํ HTML๊ณผ ํด๋ผ์ด์ธํธ์์ React๊ฐ ๋ง๋ค๋ ค๋ VDOM์ด ๋ค๋ฅด๋ฉด ์๋ฌ ๋ฐ์.
// โ Hydration Mismatch ์์ธ: ์๋ฒ/ํด๋ผ์ด์ธํธ ํ๊ฒฝ ์ฐจ์ด
function Greeting() {
// ์๋ฒ: typeof window === 'undefined' โ ๋ค๋ฅธ ๊ฒฐ๊ณผ
// ํด๋ผ์ด์ธํธ: typeof window === 'object' โ ๋ค๋ฅธ ๊ฒฐ๊ณผ
return <div>{typeof window === 'undefined' ? 'Server' : 'Client'}</div>;
}
// โ
ํด๊ฒฐ: useEffect๋ก ํด๋ผ์ด์ธํธ์์๋ง ์คํ
function Greeting() {
const [isClient, setIsClient] = useState(false);
useEffect(() => setIsClient(true), []);
return <div>{isClient ? 'Client' : 'Loading...'}</div>;
}๐๏ธ 3. Next.js App Router โ ์๋ฒ ์ปดํฌ๋ํธ์ HTML
Next.js App Router์ ์๋ฒ ์ปดํฌ๋ํธ๋ ํด๋ผ์ด์ธํธ๋ก JavaScript๋ฅผ ์ ํ ๋ณด๋ด์ง ์๊ณ HTML๋ง ์์ฑํด.
// app/posts/[id]/page.tsx โ ์๋ฒ ์ปดํฌ๋ํธ (๊ธฐ๋ณธ)
// ์ด ์ปดํฌ๋ํธ๋ ์๋ฒ์์๋ง ์คํ, ํด๋ผ์ด์ธํธ JS ๋ฒ๋ค์ ํฌํจ ์ ๋จ
async function PostPage({ params }: { params: { id: string } }) {
// ์๋ฒ์์ ์ง์ DB๋ API ํธ์ถ ๊ฐ๋ฅ
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ 'use client' ์ ์ธ ํ์ */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
);
}// components/LikeButton.tsx โ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ
"use client"; // ์ด ์ ์ธ์ด ์์ด์ผ๋ง JS๊ฐ ํด๋ผ์ด์ธํธ๋ก ์ ์ก๋จ
function LikeButton({ postId, initialLikes }: Props) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(l => l + 1)}>
โค๏ธ {likes}
</button>
);
}๊ฒฐ๊ณผ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ๊ฐ ๋ฐ๋ HTML:
PostPage์h1,p= ์๋ฒ์์ ๋ง๋ค์ด์ง ์ ์ HTML (JS ์์)LikeButton= ์ด๊ธฐ HTML์ ํฌํจ๋๋, Hydration์ ์ํ JS๊ฐ ํด๋ผ์ด์ธํธ๋ก ์ ์ก๋จ
๐ ํต์ฌ ๋น๊ตํ
| ๋ ๋๋ง ๋ฐฉ์ | ์ด๊ธฐ HTML | JS ํ์ | FCP | SEO |
|---|---|---|---|---|
| CSR | ๋น ๊ป๋ฐ๊ธฐ | ํ์ | ๋๋ฆผ | ๋ถ๋ฆฌ |
| SSR | ์ฝํ ์ธ ํฌํจ | Hydration์ฉ | ๋น ๋ฆ | ์ ๋ฆฌ |
| SSG | ์ฝํ ์ธ ํฌํจ (๋น๋ ์ ์์ฑ) | Hydration์ฉ | ๋งค์ฐ ๋น ๋ฆ | ๋งค์ฐ ์ ๋ฆฌ |
| ์๋ฒ ์ปดํฌ๋ํธ | ์ฝํ ์ธ ํฌํจ | ์ต์ํ | ๋งค์ฐ ๋น ๋ฆ | ๋งค์ฐ ์ ๋ฆฌ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ๋ค์ ์ค SEO ๊ด์ ์์ ์ด๊ธฐ HTML์ ์ฝํ ์ธ ๊ฐ ํฌํจ๋์ด ๊ตฌ๊ธ ๋ด์ด JavaScript ์คํ ์์ด๋ ํ์ด์ง ๋ด์ฉ์ ์ฝ์ ์ ์๋ ๋ ๋๋ง ๋ฐฉ์์?
- A) CSR (Create React App ๊ธฐ๋ณธ ๋ฐฉ์)
- B) SSR (Next.js getServerSideProps)
- ๊ฐ) CSR + SWR ๋ฐ์ดํฐ ํ์นญ
- ๋ผ) CSR + React Query
โ ์ ๋ต: B
๐ก ์์ธ ํด์ค:
- SSR์ ์๋ฒ์์ HTML์ ์์ฑํด์ ๋ณด๋ด์ฃผ๋ฏ๋ก ๊ตฌ๊ธ ๋ด์ด JavaScript ์์ด๋ ์ฝํ ์ธ ๋ฅผ ์ฝ์ ์ ์์ด์. CSR ๋ฐฉ์์ ์ด๊ธฐ HTML์ด ๋น ๊ป๋ฐ๊ธฐ์ด๊ณ JavaScript๊ฐ ์คํ๋์ด์ผ ์ฝํ ์ธ ๊ฐ ์ฑ์์ง๋ฏ๋ก SEO์ ๋ถ๋ฆฌํด์.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "SSR/SSG = ๊ตฌ๊ธ ๋ด์๊ฒ ์ฝํ ์ธ ๋ด๊ธด ๋ช ํจ, CSR = ๋น ๋ช ํจ"
Q2. Hydration์ด๋ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ์๋ฒ์์ ๋ ๋๋ง๋ ์ ์ HTML์ React ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ์ํ๋ฅผ ์ฐ๊ฒฐํ์ฌ ์ธํฐ๋ํฐ๋ธํ ์ฑ์ผ๋ก ๋ง๋๋ ๊ณผ์
๐ก ์์ธ ํด์ค:
- SSR๋ก ์์ฑ๋ HTML์ ์ฌ์ฉ์ ๋์ ๋ฐ๋ก ๋ณด์ด์ง๋ง, ๋ฒํผ ํด๋ฆญ์ด๋ ์ํ ๋ณํ๋ React JS๊ฐ Hydration์ ์๋ฃํด์ผ ์๋ํด์. Hydration์ ๊ธฐ์กด DOM์ ๋ค์ ๋ ๋๋งํ์ง ์๊ณ ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ์ํ๋ฅผ ์ฐ๊ฒฐํ๋ ํจ์จ์ ์ธ ๊ณผ์ ์ด์์.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์
Next.js App Router ํ๋ก์ ํธ์์
'use client'์ ์ธ ์์ด ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์๋ค.
์ด ์ปดํฌ๋ํธ์์useState๋ฅผ ์ฌ์ฉํ๋ ค๊ณ ํ๋๋ ์๋ฌ๊ฐ ๋ฌ๋ค. ์ ๊ทธ๋ฐ๊ฐ?
โ
์ ๋ต: ์๋ฒ ์ปดํฌ๋ํธ๋ ํด๋ผ์ด์ธํธ์์ ์คํ๋์ง ์์ผ๋ฏ๋ก useState, useEffect ๊ฐ์ React hooks๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ํด๋ผ์ด์ธํธ์ธก ์ํ/์ธํฐ๋์
์ด ํ์ํ๋ฉด 'use client' ๋ฅผ ์ ์ธํด์ผ ํ๋ค
๐ก ์์ธ ํด์ค:
- ์๋ฒ ์ปดํฌ๋ํธ๋ ์๋ฒ์์๋ง ์คํ๋์ด HTML์ ์์ฑํ๊ณ ํด๋ผ์ด์ธํธ๋ก JS๋ฅผ ๋ณด๋ด์ง ์์์.
useState๋ ํด๋ผ์ด์ธํธ์์ ๋ธ๋ผ์ฐ์ ๋ฉ๋ชจ๋ฆฌ์ ์ํ๋ฅผ ์ ์งํ๋ ํ ์ด๋ฏ๋ก ์๋ฒ ํ๊ฒฝ์์๋ ์๋ฏธ๊ฐ ์์ด์. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์ํ/์ด๋ฒคํธ/๋ธ๋ผ์ฐ์ API =
'use client'ํ์"
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ ๋๋์ด "์ Next.js๊ฐ SEO์ ์ข์๊ฐ" ๋ฅผ HTML ๋ ๋ฒจ์์ ์ดํดํ๋ค. CRA๋ก ๋ง๋ ์ฑ์ ๊ตฌ๊ธ ๋ด์ด ๋ณด๋ฉด <div id="root"></div> ํ ์ค์ง๋ฆฌ ํ์ด์ง๋ฅผ ๋ณด๋ ๊ฑฐ์๋ค. ๊ทธ๋ฌ๋๊น ๊ฒ์ ๊ฒฐ๊ณผ์ ์ ๋์๋ ๊ฑฐ์ง. SSR๋ก ๋ฐ๊พธ๋๊น ๊ตฌ๊ธ ๋ด์ด ์ค์ ๊ฒ์๊ธ ๋ด์ฉ์ด ๋ด๊ธด HTML์ ๋ฐ๊ฒ ๋๊ณ , 2์ฃผ ํ์ ๊ฒ์ ๊ฒฐ๊ณผ์ ์ฐ๋ฆฌ ๊ฒ์๊ธ์ด ๋์ค๊ธฐ ์์ํ๋ค.
๐ก "HTML์ ์ฒซ์ธ์์ด๋ค. SSR์ ๊ตฌ๊ธ ๋ด์๊ฒ ์ฝํ ์ธ ๊ฐ ๋ด๊ธด HTML์ ๋จผ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ, CSR์ ๋น ์ข ์ด๋ฅผ ์ฃผ๋ ๊ฒ์ด๋ค."
Hydration Mismatch๋ ์์ง๋ ๊ณ ํต์ค๋ฝ์ง๋ง... ์ด์ ์ ์๊ธฐ๋์ง๋ ์๊ฒ ๋ค. ์๋ฒ๋ ํด๋ผ์ด์ธํธ์ ์ด๊ธฐ ๋ ๋๋ง ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ React๊ฐ ํผ๋์ค๋ฌ์ํ๋ ๊ฑฐ๋ผ๋ ๊ฑธ. ์ด ์๋ฌ๋ฅผ ๋ณด๋ฉด "์, ์๋ฒ/ํด๋ผ์ด์ธํธ ๋ถ๊ธฐ๊ฐ ๋ฌธ์ ๊ตฌ๋" ํ๊ณ ๋ฐ๋ก ์ฐพ์ ์ ์๊ฒ ๋๋ค.
๐ ๋ ์์๋ณด๊ธฐ