๐ 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. ์์ฒ ๋์ด ๋ง์ดํ์ด์ง(SSR ํ์)์์ ๋ด ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ ์งฐ๋ค. ์ด ํ์ด์ง ์ต์๋จ ์ปดํฌ๋ํธ์ธ app/dashboard/layout.tsx ์ export const dynamic = "force-static" ์ ๋ถ์ฌ์ ๊ณ ์๋ก ์ต์ง ์ต์ ํ๋ฅผ ๊ฐํํ๋ ค ์๋ํ๋ค. ์์ฒ ๋์ ์์งํ ์ฝ๋๋ ๋น๋ ์์ ์ ์ด๋ค ๋ฌธ์ ๋ฅผ ๋ง์ฃผ์น๊ฒ ๋ ๊ฒ์ด๋ฉฐ ๊ทธ ์ด์ ๋ ๋ฌด์์ธ๊ฐ?
โ
์ ๋ต: Dynamic server usage: cookies ์๋ฌ๋ฅผ ๋ฟ์ผ๋ฉฐ ๋น๋๋ฅผ ๊ฑฐ๋ถํ๋ค.
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ๊ฐ์ ๋ก
force-static(SSG)์ ์ง์ ํ์์๋ ๋ถ๊ตฌํ๊ณ , ์์ ์ปดํฌ๋ํธ ์ด๋๊ฐ์์cookies()๋headers()๊ฐ์ ๋ฐํ์ ์ ์ฝ ํจ์๋ฅผ ์ฌ์ฉํ๋ค๋ฉด Next.js๋ ์ฆ์ ์๋ฌ๋ฅผ ๋ฟ์ผ๋ฉฐ ๋น๋๋ฅผ ๊ฑฐ๋ถํด. - ์ค๋ต ํผ๋๋ฐฑ: ์์ฒ ๋, "์ฟ ํค๋ ์ ์ ๊ฐ ๋ธ๋ผ์ฐ์ ๋ก ์ ์ํด์ผ๋ง ์๊ฒจ๋๋ ์ ๋ณด์ธ๋ฐ, ์ ๋ํํ ์ ์์๋ ์๋ ๋น๋ ํ์์ ๋ฏธ๋ฆฌ ์ ์ ์ผ๋ก ๊ตฌ์๋์ผ๋ผ๊ณ ๊ฐ์ํ๋?" ๋ผ๋ฉฐ ์ปดํ์ผ๋ฌ๊ฐ ๊ฐ๋ ฅํ ํญ์ํ๋ ๋ ผ๋ฆฌ ๊ตฌ์กฐ์ผ. SSR ๊ธฐ๋ฅ๊ณผ Static ๊ฐ์ ํ๋ ๋์์ ์ฑ๋ฆฝ๋ ์ ์๋ ๋ชจ์์ด์ง.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ์ ์ (Static)๊ณผ ๋์ (Dynamic)์ ๋ฌผ๊ณผ ๊ธฐ๋ฆ ๊ฐ์ ๊ฒ!
Q2. ์์๋ค ์ปค๋ฎค๋ํฐ ๋ฉ์ธ ๋๋ฌธ(/) ์ ์๋ฒฝํ โ (Static) ํ์ ์ ๋ฐ์๋ค. ๊ทธ๋ฐ๋ฐ ์ด๋ ๋ ์์ฒ ๋์ด "๋ฉ์ธ ํ์ด์ง ์ต์๋จ์ ์ค๋ ์ ์์๋ค์ด ๊ฒ์ํ ํธ๋ ๋ ํค์๋๋ฅผ ์ค์๊ฐ ํ๋ผ๋ฏธํฐ๋ก ๋ถ์ฌ์ ๋
ธ์ถํฉ์๋ค!" ๋ผ๋ฉฐ ์ปดํฌ๋ํธ Props๋ก searchParams ๋ฅผ ๋ด๋ ค๋ฐ๋ ์ฝ๋๋ฅผ ์์ฑํด ๋ฃ์๋ค. ์ด ์ฝ๋ ํ ์ค์ ๋ฉ์ธ ์๋ฒ์ ์ด๋ค ์น๋ช
์ ์ธ ๋ฆฌ์์ค ๋ณํ๋ฅผ ๋ชฐ๊ณ ์ฌ๊น?
โ
์ ๋ต: ๋น์ฉ์ด 0์ด์๋ ๋ฉ์ธ ํํ์ด์ง ์ ์ฒด๊ฐ ๋งค ์ ์๋ง๋ค ์๋ฒ CPU๋ฅผ ์๋ชจํ๋ ฦ (Dynamic) ๋ ๋๋ง์ผ๋ก ๊ฐ๋ฑ๋๋ค.
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
:
searchParams๋ ๋ธ๋ผ์ฐ์ ์ฃผ์์ฐฝ ํ๋ผ๋ฏธํฐ(?query=123)๋ฅผ ์ถ์ ํด์ผ ํ๋ฏ๋ก ๋ฐํ์์๋ง ์ ์ ์๋ Dynamic ์ ๋ณด์ผ.
๐ก ์์ธ ํด์ค: - ์๋ฆฌ ์ค๋ช
:
searchParams๋ ๋ธ๋ผ์ฐ์ ์ฃผ์์ฐฝ ํ๋ผ๋ฏธํฐ(?query=123)๋ฅผ ์ถ์ ํด์ผ ํ๋ฏ๋ก ๋ฐํ์์๋ง ์ ์ ์๋ Dynamic ์ ๋ณด์ผ. - ์ค๋ต ํผ๋๋ฐฑ: ์ด๊ฒ์ ๋ถ๋ชจ ๋ ๋ฒจ ์ปดํฌ๋ํธ์์ ์ฝ๋ ์๊ฐ, ๋น์ฉ์ด 0(Zero)์ด์๋ ๊ฑฐ๋ํ ๋ฉ์ธ ํํ์ด์ง ์ ์ฒด๊ฐ ๋งค ์ ์๋ง๋ค ์๋ฒ CPU๋ฅผ ๊น์๋จน๋
ฦ (Dynamic)๋ ๋๋ง์ผ๋ก ๋ชฝ๋ ํฌ์ ๊ฐ๋ฑ ์ฒ๋ฆฌ๋ผ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ์ํธ ๋ฆฌ๋ ๋ ์ด๋ผ๋ฉด ์ด๋ฐ ์ํฉ์์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ Leaf ๋ถ๋ถ์ผ๋ก ๊ฒ์ ํ๋ผ๋ฏธํฐ ๋ก์ง๋ง ๊ฒฉ๋ฆฌ์์ผ ๋ฉ์ธ
page.tsx์ Static ์์ฑ์ ๋์ง๊ธฐ๊ฒ ์ํธํ์ ๊ฑฐ์ผ.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ ๋ค์ํ ๋ ๋๋ง ์ ๋ต์ ๋ฐฐ์ฐ๋ฉด์ ์ํฉ์ ๋ง๋ ๋๊ตฌ๋ฅผ ๊ณ ๋ฅด๋ ๊ฒ ์ผ๋ง๋ ์ค์ํ์ง ๊นจ๋ฌ์์ด! ํนํ '๋ฏธ๋ฆฌ ๋์ฌ๋๋ ์ก์(Static)' ์ '์ฃผ๋ฌธ ์ฆ์ ๋ณถ์๋ด๋ ์๋ฆฌ(Dynamic)' ์ ๋น์ ๋ ์์ํ ์์ง ๋ชปํ ๊ฒ ๊ฐ์.
๐ก ์ค๋์ ๊ตํ: "์๋ฒ์ ํํ๋ฅผ ์ํด, Dynamic ํจ์๋ ๊ผญ ํ์ํ ๊ณณ์๋ง Suspense๋ก ๊ฒฉ๋ฆฌํ์!"
๋ด๊ฐ ๋ฌด์ฌ์ฝ ์ถ๊ฐํ cookies() ํ ์ค์ด ์๋ฒ ๋น์ฉ์ ํญ์ฆ์ํฌ ์ ์๋ค๋ ๊ฒ ์ ๋ง ๋ฌด์ญ๋๋ผ. ์์ผ๋ก๋ ๋น๋ ๋ก๊ทธ์ โ ์ ฦ ์์ด์ฝ์ ์ฐ์ธ ์ผ๊ตด ๋ณด๋ฏ ๊ผผ๊ผผํ ์ฑ๊ฒจ๋ด์ผ๊ฒ ์ด. ์ค๋์ ๋จธ๋ฆฌ๋ฅผ ๋๋ฌด ๋ง์ด ์ผ๋๋ ๋น์ด ๋จ์ด์ง๋ค. ์ง์ ๊ฐ๋ ๊ธธ์ ๋ฌ๋ฌํ ์ด์ฝ ์ฐ์ ํ๋ ์ฌ์ ๋ง์
์ผ์ง! ๋น๋ก ์ง๊ธ์ ์ค์ํฌ์ฑ์ด ์์ฒ ์ด์ง๋ง, ๋ด์ผ์ ์ค๋๋ณด๋ค ๋ '์ ์ (Static)'์ด๊ณ ์ฐ์ํ ์ฝ๋๋ฅผ ์ง๋ณด๊ฒ ์ด! ๐ฃ