๐ Next.js 3์ฅ: App Router์ 3๋ ๋ ๋๋ง ์ ๋ต (SSR, SSG, ISR)
๐ ๊ฐ์
SSR, SSG, ISR ์ธ ๊ฐ์ง ๋ ๋๋ง ์ ๋ต์ ์ฐจ์ด์ App Router์์ ๊ฐ๊ฐ์ ์ธ์ ์จ์ผ ํ๋์ง ์์๋ด ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐ ๋ ๋๋ง ์์ ์ ๋น๋ฐ: Build-time vs Run-time ๐ข
- ๐งฉ 1. Static Rendering (๊ณผ๊ฑฐ์ SSG) ๐ข
- ๐งฉ 2. Dynamic Rendering (๊ณผ๊ฑฐ์ SSR) ๐ข
- ๐งฉ 3. ISR: ๋ฐ์๋ ๋ ๋๋ง์ ๋ง๋ฒ ๐ก
- ๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ๋ด ๋ผ์ฐํธ๊ฐ Static์ธ์ง Dynamic์ธ์ง ํ์ธํ๊ธฐ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ฏ ๊ฐ์ธํ ํผ๋์ SSR ๊ฒฉ๋ฆฌ ์ ๋ต ๐ก
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ(์ ์ฒด) / ํต์ฌ ํํธ๋ง: 10๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
๋ ๋๋ง ์์ ๋ ๊ฐ์ง โ ์ ์ ๋ ๋๋ง(SSG) โ ๋์ ๋ ๋๋ง(SSR) โ ์ ์ง์ ์ฌ์์ฑ(ISR) โ ๋น๋ ๊ฒฐ๊ณผ ๊ฟํ(์์๋ค ์ปค๋ฎค๋ํฐ ์์ )
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- ๊ฐ ํ๋ฉด ์ฑ๊ฒฉ๊ณผ ์๋น์ค ์๊ตฌ์ฌํญ์ ๋ง์ถฐ ์ต์ ์ ์๋ฒ ๋ ๋๋ง ๋ฐฉ์์ ์ค์ค๋ก ์ค๊ณํ ์ ์๋ค.
- ๋น๋ ๋ก๊ทธ๋ฅผ ํตํด ์ฝ๋๊ฐ ์๋์น ์๊ฒ ์๋ฒ ์์์ ๊ณ ๊ฐ์ํค๊ณ ์์ง ์์์ง ํต์ ํ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์๋ก ์จ ์ฃผ๋์ด): "์ด? ๋ถ๋ช ํ ๋ก์ปฌ์์๋ ์ ๋๋๋ฐ... ๋ฐฐํฌํ๊ณ ๋๋๊น ํ์ฌ ์๊ฐ ํ์ด์ง ํ๋ ์ด ๋๋ง๋ค ์๋ฒ๊ฐ ๋น๋ช ์ ์ง๋ฌ์! ๐ญ"
- ์ํธ(FE ๋ฆฌ๋): "์์ฒ ๋... ์๊ฐ ํ์ด์ง ์ต์๋จ์
cookies()ํจ์๋ฅผ ๋ฃ์ผ์ จ๋๊ตฐ์. ๊ทธ ํ ์ค ๋๋ฌธ์ ์ ์ ์ผ๋ก ๊ตฌ์์ ธ์ผ ํ ํ์ด์ง๊ฐ ์ค์๊ฐ ํ๋ฒ๊ฑฐ๊ฐ ๋ ๊ฑฐ์์!"
๐ค ์ ์์์ผ ํ๋๊ฐ
์์๋ค ์ปค๋ฎค๋ํฐ์ ๋๋์ด ์ ์ ์๊ฐ ๋ชฐ๋ฆฌ๊ธฐ ์์ํ์ด.
์์ ๋์น๋ ์์ฒ (์๋ก ์จ ์ฃผ๋์ด) ๋์ ๋ฌด์ฒ ๊ธฐ๋ปค์ง๋ง, ๊ธฐ์จ๋ ์ ์. ์คํฐ๋ ๋งค์นญ ์ฑ์ "์๊ฐ ํ์ด์ง(/about)" ํ๋ ๋ณด๋ ค๊ณ ๋ค์ด์จ ์ ์ ๋ค ๋๋ฌธ์ DB ์๋ฒ CPU๊ฐ 100%๋ฅผ ์ฐ์ผ๋ฉฐ ์ ์ฒด ์๋น์ค๊ฐ ๋ง๋น๋์ด ๋ฒ๋ ธ์ด.
์์(PM/๋ฐฑ์๋) ๋์ด ์ธ์์ ์ง์ผ๋ฉฐ ๋ฌ๋ ค์์ด. "์๋, ์ผ ๋ ๋ด๋ด ๊ธ์ ํ๋ ์ ๋ณํ๋ ํ์ฌ ์๊ฐ ํ์ด์ง์ ์ ์ํ ๋ ๋ง๋ค ์ ์ด๋ฐ ๊ณผ๋ํ ๋ถํ๊ฐ ๊ฑธ๋ฆฌ๋ ๊ฑฐ์ฃ ? ๋์ฒด ์๋ฒ์์ ๊ตญ๋ฐฅ์ ์๋ก ๋์ด๋ ์ด์ ๊ฐ ๋ญก๋๊น!"
๋ฒ์ธ์ ์ด๋ฒ์๋ ์์ฒ ๋. ๊ทธ๋ ์ฟ ํค ๊ธฐ๋ฐ ๋ค๊ตญ์ด ์ฒ๋ฆฌ๋ฅผ ํ๋ต์๊ณ ์ต์์ ์์ญ์ cookies() ํจ์๋ฅผ ์ต์์์ ๋ฃ์๊ณ , ์ด๋ก ์ธํด Next.js๊ฐ ๋ฏธ๋ฆฌ ์์ฃผ ๋ง์๊ฒ ๊ตฌ์๋์๋(Static) ํ์ด์ง๋ค์ ๋ชจ์กฐ๋ฆฌ ๋ฐํ์ ์ฆ์ ๋ ๋๋ง(Dynamic)์ผ๋ก ๊ฐ๋ฑ์์ผ๋ฒ๋ฆฐ ๊ฑฐ์ผ. ๋ ๋๋ง ์ ๋ต์ ์ ๋๋ก ๋ชจ๋ฅด๋ฉด ์ด๋ฐ '์๋ฒ ๋ถํ ๋ฌธ์ '์ ๋ง๋ค๊ฒ ๋ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด? (์์๋ค ๊ตญ๋ฐฅ์ง)
๋ ๋๋ง ์ ๋ต์ ์์์ ๋ง๋ค์ด๋๋ ํ์ด๋ฐ ์ ์ฐจ์ด์ผ.
- Static Rendering (SSG): ์๋ฒฝ ์ฅ์ฌ ์ (๋น๋ ํ์)์ ๋ํ ๊ฐ๋ง์ฅ์ ๊ตญ๋ฐฅ์ ์น ๋ค ๋์ฌ๋๋ ๊ฑฐ์ผ. ์๋์ด ์ค๋ฉด 1์ด ๋ง์ ํญ ๋์ ธ์ค. ์ ์ผ ๋น ๋ฅด๊ณ ์๋ฐ์(์๋ฒ)๋ ์ ํ๋ค์ด. (ํ์ฌ ์๊ฐ, ๋ธ๋ก๊ทธ)
- Dynamic Rendering (SSR): ์ด๊ฑด ์ฆ์์์ ๋ณถ๋ ์ ์ก๋ณถ์ ์ด์ผ. ์๋์ด ์ฃผ๋ฌธ(์ ์)ํ ๋๋ง๋ค ์๋ฐ์์ด ๋ถ ์ผ๊ณ ๊ณ ๊ธฐ๋ฅผ ์๋ก ๋ณถ์. ์๋ฒ๊ฐ ๊ณ ์ํ์ง๋ง, "์ ๋น๊ณ ๋นผ์ฃผ์ธ์(๊ฐ์ธํ ์ฟ ํค)" ๊ฐ์ ์๊ตฌ๋ฅผ 100% ๋ง์ถฐ ์ค ์ ์์ด. (๋ง์ดํ์ด์ง, ์ฅ๋ฐ๊ตฌ๋)
- ISR (Incremental Static Regeneration): "์ผ์ ์๊ฐ๋ง๋ค ์๋ก ๋ฌด์ณ ๋์ค๋ ๊ฒ์ ์ด" ๊ฐ์ ๊ฑฐ์ผ. ๋ฏธ๋ฆฌ ๋ฌด์ณ๋์ง๋ง, 10๋ถ๋ง๋ค ์ฃผ๋ฐฉ์์ ์ ๊ฑธ๋ก ๋ฌด์ณ์ ์ฑ์ ๋ฃ์ด. ์ ์ ํจ๊ณผ ์๋๋ฅผ ๋ค ์ก๋ ๋ฐฉ๋ฒ์ด์ง.
๐ ๋ ๋๋ง ์์ ์ ๋น๋ฐ: Build-time vs Run-time ๐ข
๐ฏ ์ด ์น์ ์ดํ์ ๋น์ ์:
- ๋น๋ ํ์๊ณผ ๋ฐํ์์ ๊ธฐ์ ์ ์ฐจ์ด๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ ์ ์๋ค.
- ์ ๋น๋ ํ์ ์ต์ ํ๊ฐ ์๋ฒ ๋น์ฉ ์ ๊ฐ์ ํต์ฌ์ธ์ง ์ดํดํ๋ค.
๊ฐ์ฅ ํต์ฌ์ด ๋๋ ๊ธฐ์ค์ ๊ฒฐ๊ตญ "HTML์ ๋๋์ฒด ์ธ์ ๋ง๋๋๋?" ์ผ.
- Build Time (๋น๋ ํ์): ๊ฐ๋ฐ์๊ฐ ์๋ฒ์ ์ฝ๋ ๋ฐฐํฌํ๋ ค๊ณ
npm run build๋ฅผ ์ณค์ ๋.(์ฌ์ฉ์ 0๋ช ) - Run Time (๋ฐํ์): ๋น๋ ๋๋ด๊ณ ์ด์ ์ค์ผ ๋, ์ฌ์ฉ์๊ฐ ์ฐ๋ฆฌ ์ฑ ๋๋ฉ์ธ ์น๊ณ ์ ์ํ ๋ฐ๋ก ๊ทธ ์๊ฐ.
๐งฉ 1. Static Rendering (๊ณผ๊ฑฐ์ SSG) ๐ข
๐ฏ ์ด ์น์ ์ดํ์ ๋น์ ์:
- Static Rendering์ด ๊ธฐ๋ณธ๊ฐ(Default)์ผ๋ก ๋์ํ๋ ์๋ฆฌ๋ฅผ ์๋ค.
- ์ด๋ค ์ฝ๋๊ฐ Static ์ต์ ํ๋ฅผ ๊นจ๋จ๋ฆฌ๋์ง ์ธ์งํ๋ค.
๐ ์ฉ์ด: SSG(Static Site Generation) โ ์ฑ์ ๋ฐฐํฌ ์ (๋น๋ ํ์)์ ๋ฏธ๋ฆฌ HTML์ ์น ๋ค ์์ฑํด๋๋ ๋ฐฉ์.
์ด๊ฒ ๋ญ๊ฐ? & ์ ํ์ํ๊ฐ?
์๋ฌด๋ฐ ํน์ด ์ค์ ์์ด ์ง ์์ Server Component๋ Next.js๊ฐ ์๋์ผ๋ก Static Rendering์ผ๋ก ๊ฐ์ฃผํด.
์์ฒ ๋์ด ์ง๋ /about ํ์ด์ง์ฒ๋ผ ์ ์ ์ธ ๋ฌธ์๋ค์ ํ ๋ฒ๋ง ๊ตฌ์์ ์ ์ธ๊ณ CDN ์ ๋ณต์ฌํด๋๋ฉด, ์๋ฌด๋ฆฌ 10๋ง ๋ช
์ด ๋ชฐ๋ ค๋ ์๋ฒ ๋ถํ๊ฐ 0(Zero) ์ด๋ ๋ป์ด์ง.
โ ์์งํ ์ฝ๋ (Naive Approach)
ํ์ด์ง๋ฅผ CSR(React) ๋ก ๋ง๋ค์ด ๊ตณ์ด ํด๋ผ์ด์ธํธ์ ํต์ ๋ฅ๋ ฅ์ ๋ญ๋นํ๊ฒ ํจ.
โ ์ฐ์ํ ์ฝ๋ (Pro Approach) - ์๋ฒฝํ Static ์ต์ ํ
// app/about/page.tsx
// ๐ฆ ์ํธ: "์๋ฌด๋ฐ ๋์ ํจ์๊ฐ ์์ผ๋, ์ด ํ์ด์ง๋ ๋น๋ ๋ ๋ฑ ํ ๋ฒ๋ง ๊ตฌ์์ง๋๋ค."
export default function AboutPage() {
return (
<main>
<h1>๋ํ๋ฏผ๊ตญ No.1 ์คํฐ๋ ๋งค์นญ, ์์๋ค ์ปค๋ฎค๋ํฐ</h1>
</main>
)
}๐งฉ 2. Dynamic Rendering (๊ณผ๊ฑฐ์ SSR) ๐ข
๐ฏ ์ด ์น์ ์ดํ์ ๋น์ ์:
- ์ด๋ค ํจ์๊ฐ ํ์ด์ง๋ฅผ SSR๋ก '๊ฐ์ ๊ฐ๋ฑ'์ํค๋์ง ๋ฆฌ์คํธ๋ฅผ ๊ธฐ์ตํ๋ค.
- Dynamic ๋ ๋๋ง์ด ํ์ํ ํ์ฐ์ ์ธ ์ํฉ์ ๊ตฌ๋ถํ ์ ์๋ค.
๐ ์ฉ์ด: SSR(Server-Side Rendering) โ ์ฌ์ฉ์๊ฐ ์ ์ํ๋ ๊ทธ ๋ฐํ์ ์์ ์, ๊ทธ ์ฌ๋๋ง์ ์ํ ์ต์ ๋ฐ์ดํฐ์ ํค๋๋ก HTML์ ๋ผ๋ถํฐ ๊ตฌ์์ฃผ๋ ๋ฐฉ์.
์ธ์ ๊ฐ์ ์ ํ๋๋๊ฐ?
๋ด ํ์ด์ง๊ฐ Static์์ ๋น์ผ Dynamic SSR๋ก ๊ฐ๋ฑ๋๋ ์กฐ๊ฑด์ ๋ช ํํด. "Next.js๊ฐ ๋ฏธ๋ฆฌ ์ ์ ์๋ ์ค์๊ฐ ์ ์ ์ ๋ณด"๋ฅผ ๊ฑด๋๋ฆฌ๋ ์๊ฐ!
cookies()์ฝ๊ธฐheaders()(์ ์ ์ Agent ๋ฑ) ์ฝ๊ธฐsearchParams๋ฅผ ํ์ด์ง ํ๋กญ์ค๋ก ๋ฐ์ ๋ ๋๋ง์ ์ธ ๋
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
export default async function DashboardPage() {
// ๐ฃ ์์ฒ : "์ํญ, ์ฟ ํค๋ฅผ ์ฝ๋ ์ด ๋ผ์ธ์ด ์ถ๊ฐ๋๋ ์๊ฐ,
// ๋น๋ ํ์์๋ ์ด๊ฒ ๋๊ตฌ ์ฟ ํค์ธ์ง ์ ์๊ฐ ์์ผ๋ ๋ฐํ์ SSR ๋ ๋๋ง์ด ๊ฐ์ ๋๋๊ตฌ๋!"
const cookieStore = cookies()
const userToken = cookieStore.get('auth_token')
return (
<main>
<h1>๋ด ์คํฐ๋ ํํฉ</h1>
</main>
)
}๐งฉ 3. ISR: ๋ฐ์๋ ๋ ๋๋ง์ ๋ง๋ฒ ๐ก
๐ฏ ์ด ์น์ ์ดํ์ ๋น์ ์:
revalidate์ต์ ์ ํตํด ์ ์ ํ์ด์ง๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฐฑ์ ํ ์ ์๋ค.- Stale-While-Revalidate ์ ๋ต์ ์ฅ๋จ์ ์ ์ดํดํ๋ค.
์์(๋์์ด๋) ๋์ด ์๊ตฌํ์ด. "์ํธ ๋, ๋ฉ์ธ ํ๋ฉด์ '์ค์๊ฐ ๊ธ์์น ์ธ๊ธฐ ์คํฐ๋' ๋ชฉ๋ก์ ํ 10๋ถ์ ํ ๋ฒ ์ ๋๋ง ๊ฐฑ์ ๋ผ๋ ์ถฉ๋ถํ ๊ฑฐ ๊ฐ์์. ์ ์์๊ฐ ๋ชฐ๋ฆด ๋ ๋ก๋ฉ ๊ฑธ๋ฆฌ๋ ๊ฑด ์ ๋ ์ซ๊ณ ์!"
์ํธ(FE ๋ฆฌ๋) ๋์ ์ฉ ์์ผ๋ฉฐ ๋๋ตํ์ด. "ISR๋ก ์บ์๋ฅผ ์น๋ฉด ์๋ฒฝํ์ฃ !"
ISR ๋์ ์๋ฆฌ
์ ์ ํ์ด์ง์ ๊ฐ๋ ฅํ ์๋๋ฅผ ์ ์งํ๋ฉด์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฃผ๊ธฐ์ ์ผ๋ก ์บ์๋ฅผ ๊ฐฑ์ ํด.
// app/trending/page.tsx
export default async function TrendingPage() {
// โ
next: { revalidate: 600 } -> 10๋ถ(600์ด)์ง๋ฆฌ ISR ์ ๋ต ์ ์ธ!
const res = await fetch('https://api.youngsu.com/bestsellers', {
next: { revalidate: 600 }
})
const trends = await res.json()
// ๐ฆ ์ํธ: "์ฌ์ฉ์๋ 10๋ถ ๋์ ๊ตฌ์์ง HTML์ ๊ด์์ผ๋ก ๋ฐ๊ณ , 10๋ถ์ด ์ง๋๋ฉด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ ๊ตญ๋ฐฅ์ด ๋์ฌ์ง๋๋ค."
return <ul>{trends.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}[์๋์ด ๊ด์ ์ ISR์ ํจ์ ์ฃผ์]
ISR์ ๋จ์ ์ ์๋ฒฝํ ์ค์๊ฐ์ฑ์ ๋ณด์ฅํ ์ ์๊ณ , ๋๊ตฐ๊ฐ ํฌ์์ 1๋ช
์ด ์ง๋๊ฐ ์ดํ์์ผ ์ ์ปจํ
์ธ ๋ก ๋ฎ์ด์์์ง๋ค๋ ๊ฑฐ์ผ(Stale-While-Revalidate ์ํคํ
์ฒ ํ๊ณ). ์ฆ๊ฐ ๋ฌดํจํ๊ฐ ํ์ํ ๋ ํ๋ ๋ค๋ฃฐ revalidateTag ๋ฑ ๊ณ ๊ธ ๊ธฐ๋ฒ์ด ์๊ตฌ๋ผ.
๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ๋ด ๋ผ์ฐํธ๊ฐ Static์ธ์ง Dynamic์ธ์ง ํ์ธํ๊ธฐ
๐ฏ ์ด ์น์ ์ดํ์ ๋น์ ์:
npm run build๊ฒฐ๊ณผ๋ฌผ์ ๋ณด๊ณ ๋ด ์ฝ๋์ ๋ ๋๋ง ์ ๋ต์ ํ๋ ํ ์ ์๋ค.
์ํธ ๋์ด ์์ฒ ๋์๊ฒ ๊ฐ์ฅ ๋จผ์ ๊ฐ๋ฅด์น ๊ฒ์ ๋ฐ๋ก ๋น๋ ๋ก๊ทธ ์ฝ๋ ๋ฒ์ด์์ด.
npm run build๋ค ๋๋๋ฉด ๋์๋ณด๋์ ์ด๋ฐ ๋ฉ์ง ๊ฒฐ๊ณผ๊ฐ ๋ .
Route (app) Size First Load JS
โ โ / 142 B 84.1 kB
โ โ /about 182 B 84.2 kB
โ ฦ /dashboard 312 B 84.3 kB
โ โ /trending 240 B 84.2 kB
โ (Static) automatically rendered as static HTML (uses no initial props)
ฦ (Dynamic) server-rendered on demand using Node.js
โ (ISR) incremental static regeneration (uses revalidate in fetch())| ๊ธฐํธ | ์ ๋ต | ์๋ฒ ๋ถํ ์์ค |
|---|---|---|
โ | Static | ์ฌ์ค์ ๋น์ฉ ์ ๋ก(0). |
ฦ | Dynamic | ํ์ (ฦ) ๋ชจ์. ์์ฒญ ์๋ง๋ค ์คํ๋๋ฏ๋ก ํธ๋ํฝ ํ๋ฉด ์กฐ์ฌํด์ผ ํจ! |
โ | ISR | ์ค์ํธ ์คํ. ์ ์ ํ์ผ ๋ฐฐ๋ฌ + ๊ฐํ์ ๊ฐฑ์ |
๐ฏ ๊ฐ์ธํ ํผ๋์ SSR ๊ฒฉ๋ฆฌ ์ ๋ต ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๊ฐ์ธํ ํผ๋๊ฐ ์ SSR์ด ํ์ํ์ง ์ด์ ๋ฅผ ์ค๋ช ํ ์ ์๋ค.
- Suspense ๊ฒฝ๊ณ๋ก SSR ํ์ํ ๋ถ๋ถ๋ง ๊ฒฉ๋ฆฌํ๊ณ ๋๋จธ์ง Static์ ์ ์งํ๋ ํจํด์ ์ ์ฉํ ์ ์๋ค.
useSearchParams๋ฅผ ์ธ ๋ ์ ์ฒด ํ์ด์ง๊ฐ Dynamic์ผ๋ก ๊ฐ๋ฑ๋์ง ์๋๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์๋ค.
๊ฐ์ธํ ํผ๋๋ ์ SSR์ด์ด์ผ ํ ๊น?
์์๋ค ์ปค๋ฎค๋ํฐ ๋ฉ์ธ ํผ๋ ํ์ด์ง๋ ๋ก๊ทธ์ธํ๋ฉด "๋ด๊ฐ ํ๋ก์ฐํ๋ ์คํฐ๋ ๊ทธ๋ฃน" ์ ์ต์ ๊ฒ์๊ธ๋ง ๋ณด์ฌ์ค์ผ ํด. ์ด ํผ๋๋ ์๋ ์ด์ ๋ก ๋น๋ ํ์์ ๋ฏธ๋ฆฌ ๊ตฌ์๋ ์๊ฐ ์์ด:
- ์ฌ์ฉ์๋ง๋ค ๋ค๋ฅด๋ค โ ์์ฒ ๋์ ํผ๋๋ "ํ๋ก ํธ์๋" ์คํฐ๋๋ง ๋ณด์ด๊ณ , ์์ ๋์ ํผ๋๋ "๋์์ธ" ์คํฐ๋๋ง ๋ณด์ฌ. ๋ฏธ๋ฆฌ ๊ตฌ์ธ ์๊ฐ ์์ง.
- ์ฟ ํค/์ธ์
์ ์์กดํ๋ค โ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์๋ฒ์์ ์ฟ ํค๋ก ํ์ธํด์ผ ํด.
cookies()ํจ์๋ฅผ ์ฐ๋ ์๊ฐ Static ๋ถ๊ฐ. - ์ค์๊ฐ์ฑ์ด ํ์ํ๋ค โ ๋ฐฉ๊ธ ์ฌ๋ผ์จ ๊ฒ์๊ธ์ด ์์ผ๋ฉด ๋ฐ๋ก ๋ณด์ฌ์ผ ํด.
์ด ๊ฒฝ์ฐ ํผ๋ ์ปดํฌ๋ํธ๋งํผ์ SSR(Dynamic Rendering) ์ด ๋ง์. ์ ์ํ ๋๋ง๋ค ์๋ฒ์์ ์ฟ ํค๋ฅผ ์ฝ๊ณ , ํด๋น ์ ์ ์ ๊ตฌ๋ ์คํฐ๋ ๋ชฉ๋ก์ DB์์ ๊บผ๋ด์ HTML์ ์ฆ์์ผ๋ก ๋ง๋ค์ด์ผ ํ๊ฑฐ๋ .
๋ฌธ์ : ํผ๋ ํ๋ ๋๋ฌธ์ ํ์ด์ง ์ ์ฒด๊ฐ Dynamic์ผ๋ก ๊ฐ๋ฑ๋๋ค!
์ด ๊ฐ์ธํ ํผ๋๋ฅผ page.tsx ์ต์๋จ์ ๋ฐ๋ก ๋ฃ์ผ๋ฉด ํฐ์ผ ๋.
// โ ์์งํ ์ฝ๋: ๋ฉ์ธ ํ์ด์ง ์ ์ฒด๊ฐ Dynamic์ผ๋ก ๊ฐ๋ฑ๋จ
// app/feed/page.tsx
export default async function FeedPage() {
const cookieStore = await cookies() // ๐ฃ ์์ฒ : "์๋ฌด ์๊ฐ ์์ด ์ฌ๊ธฐ์ ์ฟ ํค๋ฅผ ์ฝ์๋๋..."
const userId = cookieStore.get('user-id')?.value
const feed = await db.posts.findMany({ where: { authorFollowedBy: userId } })
return (
<div>
<HeroSection /> {/* Static์ผ๋ก ์ถฉ๋ถํ๋ฐ, ๋ฉ๋ฌ์ Dynamic์ผ๋ก ๊ฐ๋ฑ๋จ */}
<TrendingTopics /> {/* ISR์ด๋ฉด ์ถฉ๋ถํ๋ฐ, ์ญ์ Dynamic์ผ๋ก ๊ฐ๋ฑ๋จ */}
<PersonalizedFeed feed={feed} /> {/* ์ด๊ฒ๋ง Dynamic์ด ํ์ํ๋ฐ */}
</div>
)
}๋น๋ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด /feed ๊ฐ ฦ (Dynamic) ์ผ๋ก ํ์๋์ด, 1๋ง ๋ช
์ด ๋์์ ์ ์ํ๋ฉด 1๋ง ๋ฒ ์ฆ์ ๋ ๋๋ง์ด ๋ฐ์ํด. HeroSection ๊ณผ TrendingTopics ๋ Static์ผ๋ก ์ถฉ๋ถํ๋๋ฐ ์ต์ธํ๊ฒ ํฌ์๋ ๊ฑฐ์ผ.
ํด๊ฒฐ์ฑ : Suspense ๊ฒฝ๊ณ๋ก SSR ๋ถ๋ถ๋ง ์ ๋ฐ ๊ฒฉ๋ฆฌ
์ํธ ๋ ์ด ์ ์ํ ์ค์ ์ํคํ ์ฒ์ผ. SSR์ด ํ์ํ ์ปดํฌ๋ํธ๋ง ๋น๋๊ธฐ ์๋ฒ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๊ณ , ๋๋จธ์ง๋ Static์ผ๋ก ์ ์ง ํ๋ ๋ฐฉ๋ฒ์ด์ผ.
// โ
์ฐ์ํ ์ฝ๋: SSR ๊ฒฉ๋ฆฌ + Static ๋ณด์กด
// app/feed/page.tsx
import { Suspense } from 'react'
import PersonalizedFeed from '@/components/PersonalizedFeed'
// ๐ฆ ์ํธ: "page.tsx ์์ฒด๋ ์ ์ (Static)์ผ๋ก ๋จ๊ฒจ๋๊ณ , ์ฟ ํค ์ฌ์ฉ์ฒ๋ง ๋๋ ค๋
๋๋ค."
export default function FeedPage() {
return (
<div>
<HeroSection />
<TrendingTopics />
<Suspense fallback={<FeedSkeleton />}>
<PersonalizedFeed /> {/* ์ด ์ปดํฌ๋ํธ ์์์๋ง cookies() ์ฌ์ฉ */}
</Suspense>
</div>
)
}// app/components/PersonalizedFeed.tsx
// ์ด ์ปดํฌ๋ํธ๋ง SSR (Dynamic)
import { cookies } from 'next/headers'
export default async function PersonalizedFeed() {
const cookieStore = await cookies() // โ ์ฌ๊ธฐ์๋ง ์ฟ ํค ์ ๊ทผ
const userId = cookieStore.get('user-id')?.value
if (!userId) return <LoginPrompt />
const posts = await db.posts.findMany({
where: { authorFollowedBy: userId },
orderBy: { createdAt: 'desc' },
take: 20,
})
return <ul>{posts.map(p => <PostCard key={p.id} post={p} />)}</ul>
}๋น๋ ๊ฒฐ๊ณผ ๋น๊ต:
| ์์งํ ์ฝ๋ | Suspense ๊ฒฉ๋ฆฌ ์ฝ๋ | |
|---|---|---|
/feed ์ ์ฒด | ฦ Dynamic (๋งค๋ฒ ์๋ฒ ์คํ) | โ Static + ์คํธ๋ฆฌ๋ฐ |
| HeroSection | ๋งค๋ฒ ์ฌ๋ ๋ | CDN์์ ์ฆ์ ์๋ต โ |
| PersonalizedFeed | Dynamic | Dynamic (ํ์ง๋ง ๋๋จธ์ง๋ Static ์ ์ง) โ |
useSearchParams ๋ ๊ฐ์ ์๋ฆฌ๋ก ๊ฒฉ๋ฆฌํด์ผ ํด
ํด์ฆ 2๋ฒ ํด์ค์์ ๋์จ searchParams ๋ ๋ง์ฐฌ๊ฐ์ง์ผ. useSearchParams() ๋ฅผ ์ฐ๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๊ฐ page.tsx ์๋จ์ ์์ผ๋ฉด ์ ์ฒด ํ์ด์ง๊ฐ Dynamic์ผ๋ก ๊ฐ๋ฑ๋ผ.
// โ
searchParams ๊ฒฉ๋ฆฌ ํจํด
// app/feed/page.tsx โ Static ์ ์ง
export default function FeedPage() {
return (
<div>
<HeroSection />
<Suspense fallback={<FilterSkeleton />}>
<FilteredFeed /> {/* searchParams ์ฐ๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ฅผ ๊ฒฉ๋ฆฌ */}
</Suspense>
</div>
)
}// app/components/FilteredFeed.tsx
'use client'
import { useSearchParams } from 'next/navigation'
export default function FilteredFeed() {
const searchParams = useSearchParams()
const category = searchParams.get('category') // ?category=frontend
// ...
}โ ๏ธ ์ฃผ์:
useSearchParams()๋ฅผ ์ฐ๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๋ฅผ Suspense๋ก ๊ฐ์ธ์ง ์์ผ๋ฉด Next.js๊ฐ ๋น๋ ๊ฒฝ๊ณ ๋ฅผ ๋ด๋ณด๋ด. ๋ฐ๋์<Suspense>๊ฒฝ๊ณ ์์ ๋ฃ์ด์ผ ํด.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
Dynamic์ด ํ์ํ ๋ถ๋ถ๋ง<Suspense>์์ ์๋ฒ ์ปดํฌ๋ํธ(์ฟ ํค ์ ๊ทผ)๋ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ(searchParams)๋ก ๊ฒฉ๋ฆฌํด. ๋๋จธ์ง๋ Static์ด ์ ์ง๋ผ์ CDN์ด ๋์ ์ฒ๋ฆฌํด์ค ๊ฑฐ์ผ.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- SSG (
โ): ๋ณํ์ง ์๋ ํ ์คํธ์ ํ์ฌ ์๊ฐ๋ ์๋ฌ ํผ. ์ต์์ ์๋. - SSR (
ฦ): ์ฟ ํค ๋ฑ ๊ฐ์ธํ๊ฐ ๋ฌด์กฐ๊ฑด ํ์ํ ๋์๋ณด๋. - ISR (
โ): ์ค-์ค์๊ฐ์ด ํ์ํ ์ผํ๋ชฐ ๋ฒ ์คํธ์ ๋ฌ, ์ปค๋ฎค๋ํฐ ์ธ๊ธฐ๊ธ.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์ ์ ๋ ๋๋ง๊ณผ ๋์ ๋ ๋๋ง์ ๊ฐ๋ฅด๋ ํต์ฌ ์ง๋ฌธ์?
โ ์ ๋ต: ์์ฒญ๋ง๋ค ๋ฌ๋ผ์ง๋ ์ ๋ ฅ์ด ๋ ๋๋ง ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๊พธ๋๊ฐ์ด๋ค.
๐ก ์์ธ ํด์ค: cookies(), headers(), ์ฌ์ฉ์๋ณ ๊ถํ์ฒ๋ผ ์์ฒญ์ ๋ฌถ์ธ ๊ฐ์ด ํ์ํ๋ฉด ๋์ ๋ ๋๋ง์ด ์์ฐ์ค๋ฝ๋ค. ๋ชจ๋์๊ฒ ๊ฐ์ ๊ฒฐ๊ณผ๋ผ๋ฉด ์ ์ ๋ ๋๋ง์ด๋ ISR์ ๊ฒํ ํ ์ ์๋ค.
Q2. ISR์ด ์ด์ธ๋ฆฌ๋ ๋ฐ์ดํฐ๋ ์ด๋ค ๋ฐ์ดํฐ์ธ๊ฐ?
โ ์ ๋ต: ๋ชจ๋์๊ฒ ๊ฐ์ง๋ง ์ผ์ ์ฃผ๊ธฐ๋ก ์๋ก์์ ธ์ผ ํ๋ ๋ฐ์ดํฐ๋ค.
๐ก ์์ธ ํด์ค: ๊ณต์ง ๋ชฉ๋ก์ด๋ ์ธ๊ธฐ ๊ธ์ฒ๋ผ ๋ช ๋ถ ๋ฆ์ด๋ ๊ด์ฐฎ์ ๋ฐ์ดํฐ๋ revalidate ์๊ฐ์ ๋ ์ ์๋ค. ๋ด ์๋ฆผ์ฒ๋ผ ์ฌ์ฉ์๋ณ ์ฆ์์ฑ์ด ํ์ํ ๋ฐ์ดํฐ์๋ ๋ง์ง ์๋๋ค.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์: ํ์๋ ๊ณต์ง ๋ชฉ๋ก๊ณผ ๋ด ์๋ฆผ ์๊ฐ ํจ๊ป ์๋ค. ๋ ๋๋ง ์ ๋ต์ ์ด๋ป๊ฒ ๋๋๊น?
โ ์ ๋ต: ๊ณต์ง ๋ชฉ๋ก์ ์ ์ /ISR ํ๋ณด๋ก, ๋ด ์๋ฆผ ์๋ ๋์ ์์ญ์ผ๋ก ๋ถ๋ฆฌํ๋ค.
๐ก ์์ธ ํด์ค: ํ ํ๋ฉด ์์์๋ ๋ฐ์ดํฐ ์ฑ๊ฒฉ์ ๋ค๋ฅผ ์ ์๋ค. ์ํธ๋ ํ์ด์ง ์ ์ฒด๋ฅผ ํ ๋จ์ด๋ก ๋ถ๋ฅด๊ธฐ ์ ์ ๊ฐ ๋ฐ์ดํฐ์ ์ ์ ๋์ ๊ฐ์ธํ ์ฌ๋ถ๋ฅผ ๋๋๋ผ๊ณ ์กฐ์ธํ ๊ฒ์ด๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ํ์ด์ง๋ฅผ ๋น ๋ฅด๊ฒ ๋ง๋ค์ง ๋์ ์ผ๋ก ๋ง๋ค์ง ๊ฐ์ผ๋ก ๊ณ ๋ฅด์ง ์๊ฒ ๋๋ค.
๐ก "๋ ๋๋ง ์ ๋ต์ ์ฑ๋ฅ ์ทจํฅ์ด ์๋๋ผ ๋ฐ์ดํฐ๊ฐ ์ธ์ ๋๊ตฌ์๊ฒ ๋ฌ๋ผ์ง๋์ง์ ๊ฒฐ๊ณผ๋ค."
๋ค์ ์ค๊ณ์์๋ ๋ฐ์ดํฐ๋ง๋ค ๊ณต๊ฐ์ฑ, ์ ์ ๋, ์์ฒญ ์์กด์ฑ์ ๋จผ์ ํ๋ก ๋๋๊ฒ ๋ค.