TIL

Next.js - Pages Router(2)

0. Intro ๐ŸŽฏ ์ง€๋‚œ ๊ธ€์—์„œ๋Š” Pages Router์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ๋ Œ๋”๋ง ๋ฐฉ์‹๊ณผ ๋”๋ถˆ์–ด RSC ๊ฐœ๋…๊นŒ์ง€ ํญ๋„“๊ฒŒ ์‚ดํŽด๋ดค์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ๋ฐฉ์‹์ด ์–ด๋–ค ๊ณ ๋ฏผ ์†์—์„œ ๋“ฑ์žฅํ–ˆ๋Š”์ง€, ์–ด๋–ค ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ์‹œ๊ฐ„์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋ Œ๋”๋ง ๋ฐฉ์‹์ด '

2025๋…„ 12์›” 26์ผ15min read

๋ Œ๋”๋ง์— ๋Œ€ํ•œ ๋…ผ์˜๋Š” ์•„๋ž˜์˜ ๊ธ€์„ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

reference: https://velog.io/@minkwan/Next.js-Pages-Router1


0. Intro ๐ŸŽฏ

์ง€๋‚œ ๊ธ€์—์„œ๋Š” Pages Router์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ๋ Œ๋”๋ง ๋ฐฉ์‹๊ณผ ๋”๋ถˆ์–ด RSC ๊ฐœ๋…๊นŒ์ง€ ํญ๋„“๊ฒŒ ์‚ดํŽด๋ดค์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ๋ฐฉ์‹์ด ์–ด๋–ค ๊ณ ๋ฏผ ์†์—์„œ ๋“ฑ์žฅํ–ˆ๋Š”์ง€, ์–ด๋–ค ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ์‹œ๊ฐ„์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋ Œ๋”๋ง ๋ฐฉ์‹์ด '์–ด๋–ป๊ฒŒ ํ™”๋ฉด์„ ๊ทธ๋ ค๋‚ผ ๊ฒƒ์ธ๊ฐ€'์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด๋ผ๋ฉด, ์˜ค๋Š˜ ๋‹ค๋ฃฐ Data Fetching์€ ๊ทธ ํ™”๋ฉด์„ ์ฑ„์šธ '๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋””์„œ, ์–ธ์ œ, ์–ด๋–ป๊ฒŒ ๊ฐ€์ ธ์˜ฌ ๊ฒƒ์ธ๊ฐ€'์— ๊ด€ํ•œ ๋…ผ์˜์ž…๋‹ˆ๋‹ค.

Pages Router ํ™˜๊ฒฝ์—์„œ๋Š” ๊ฐ ๋ Œ๋”๋ง ์ „๋žต์— ๋งž์ถฐ ๋ฐ์ดํ„ฐ๋ฅผ fetching ํ•˜๋Š” ํŠน์ˆ˜ํ•œ ํ•จ์ˆ˜๋“ค์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋นŒ๋“œ ํƒ€์ž„์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ค๋Š” getStaticProps, ๋งค ์š”์ฒญ๋งˆ๋‹ค ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ํ™•๋ณดํ•˜๋Š” getServerSideProps, ๋™์  ๊ฒฝ๋กœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ getStaticPaths ๋“ฑ์˜ ๊ฐœ๋…์ด ์žˆ์ฃ .

ํ•™์Šตํ•œ ๋ Œ๋”๋ง ๊ฐœ๋…์„ ๊ธฐ๋ฐ˜์œผ๋กœ, Next.js Pages Router์˜ ํ•ต์‹ฌ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š” Data Fetching ๋ฉ”์„œ๋“œ๋“ค์˜ ๊ตฌ์ฒด์ ์ธ ๋™์ž‘ ์›๋ฆฌ์™€ ํ™œ์šฉ๋ฒ•์„ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


1. getStaticProps ๐ŸŽฏ

1-1. getStaticProps ์‚ฌ์šฉ โœ…

ํŽ˜์ด์ง€์—์„œ ์•„๋ž˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด getStaticProps ํ•จ์ˆ˜๋ฅผ export ํ•˜๋ฉด, Next.js๋Š” ๋นŒ๋“œ ํƒ€์ž„์— ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฌผ(props)์„ ์‚ฌ์šฉํ•ด HTML ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜๋Š” Pre-Rendering์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

js
export async function getStaticProps() {
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  const repo = await res.json()
  return { props: { repo } }
}
 
export default function Page({ repo }) {
  return repo.stargazers_count
}

์ด ๊ณผ์ •์—์„œ getStaticProps๋Š” ์˜ค์ง ์„œ๋ฒ„ ์ธก์—์„œ๋งŒ ์‹คํ–‰๋˜๋ฉฐ, ๋นŒ๋“œ ์‹œ์ ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง„ ์ •์  HTML๊ณผ JSON ํŒŒ์ผ์„ ํ•จ๊ป˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ ์ •์  ์ž์‚ฐ์€ CDN์— ์บ์‹ฑ ๋˜์–ด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งค์šฐ ๋น ๋ฅด๊ฒŒ ์ „๋‹ฌ๋˜๋ฏ€๋กœ, getStaticProps๋Š” SSG(Static Site Generation) ๋ฐฉ์‹์„ ๊ตฌํ˜„ํ•˜๋Š” ํ•ต์‹ฌ ๋ฉ”์„œ๋“œ๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Pitfall โš ๏ธ

๋ Œ๋”๋ง ๋ฐฉ์‹๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ชจ๋“  props๊ฐ€ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋˜๋ฉฐ, ์ด๋ฅผ ์ดˆ๊ธฐ HTML์„ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ Hydration ๋  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ด์„œ๋Š” ์•ˆ ๋˜๋Š” ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” props์— ํฌํ•จํ•˜์—ฌ ์ „๋‹ฌํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

1-2. getStaticProps ์‚ฌ์šฉ ์‹œ์  โœ…

getStaticProps๋Š” ํ•œ๋งˆ๋””๋กœ '๋ˆ„๊ฐ€ ์ ‘์†ํ•ด๋„ ๋˜‘๊ฐ™์€ ํ™”๋ฉด์„ ๋ณด์—ฌ์ค„ ๋•Œ' ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ์„ฑ๋Šฅ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๋Œ€ํ‘œ์ ์œผ๋กœ ๋„ค ๊ฐ€์ง€ ์ƒํ™ฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋นŒ๋“œ ์‹œ์ ์— ๋ฐ์ดํ„ฐ ์ค€๋น„๊ฐ€ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ

์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๊ธฐ ์ „, ์ฆ‰ ์„œ๋น„์Šค๋ฅผ ๋นŒ๋“œ ํ•˜๋Š” ์‹œ์ ์— ์ด๋ฏธ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚ผ ๋ฐ์ดํ„ฐ๋ฅผ ํ™•์ • ์ง€์„ ์ˆ˜ ์žˆ์„ ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์™ธ๋ถ€ CMS์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒฝ์šฐ

Contentful, Strapi์ฒ˜๋Ÿผ ํ™”๋ฉด์€ ์—†๊ณ  ๋ฐ์ดํ„ฐ๋งŒ API๋กœ ์ „์†กํ•ด ์ฃผ๋Š” ์™ธ๋ถ€ ํ—ค๋“œ๋ฆฌ์Šค CMS์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์ฝ˜ํ…์ธ (๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ, ๊ณต์ง€์‚ฌํ•ญ ๋“ฑ)๋ฅผ ๋ถˆ๋Ÿฌ์™€ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•  ๋•Œ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๊ฐ•๋ ฅํ•œ SEO์™€ ๋น ๋ฅธ ์†๋„๊ฐ€ ํ•„์ˆ˜์ ์ธ ๊ฒฝ์šฐ

๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”๊ฐ€ ์ค‘์š”ํ•˜๊ณ , ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋งค์šฐ ๋นจ๋ผ์•ผ ํ•˜๋Š” ํŽ˜์ด์ง€์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. HTML๊ณผ JSON ํŒŒ์ผ์„ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด CDN์— ์ €์žฅํ•ด๋‘๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ์ž์—๊ฒŒ ์ฆ‰์‹œ ํ™”๋ฉด์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๋งˆ๋‹ค ๋‚ด์šฉ์ด ๋‹ฌ๋ผ์ง€์ง€ ์•Š๋Š” ๊ฒฝ์šฐ

์žฅ๋ฐ”๊ตฌ๋‹ˆ, ํ”„๋กœํ•„์ฒ˜๋Ÿผ ๊ฐœ์ธํ™”๋œ ์ •๋ณด๊ฐ€ ์•„๋‹ˆ๋ผ, ๋ชจ๋“  ๋ฐฉ๋ฌธ์ž์—๊ฒŒ ๋™์ผํ•˜๊ฒŒ ๋ณด์ด๋Š” ๊ณต์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋ Œ๋”๋ง ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

1-3. getStaticProps ์‹คํ–‰ ์‹œ์  โœ…

getStaticProps๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹คํ–‰๋˜์ง€ ์•Š๊ณ , ์˜ค์ง ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋œ๋‹ค๋Š” ์ ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. getStaticProps ๋‚ด๋ถ€์— ์ž‘์„ฑํ•œ ๋กœ์ง์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ ์ฝ”๋“œ๋Š” ๋ฐฐํฌ ์‹œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฐ›๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค ํŒŒ์ผ์—์„œ ์™„์ „ํžˆ ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค. getStaticProps๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ next build ์‹œ์ ์— ์‹คํ–‰๋˜์–ด ์„œ๋น„์Šค์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ •์  ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ๋Š” ์„ฑ๋Šฅ ํŽธ์˜๋ฅผ ์œ„ํ•ด ๋งค ์š”์ฒญ๋งˆ๋‹ค ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ SSG ๋ฐฉ์‹ ์™ธ์—, ISR์ด๋‚˜ ๋™์  ๊ฒฝ๋กœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์‹คํ–‰ ์‹œ์ ์ด ์กฐ๊ธˆ ๋” ๋‹ค์–‘ํ•ด์ง‘๋‹ˆ๋‹ค. ์ด๋•Œ๋Š” revalidate ์˜ต์…˜์„ ํ†ตํ•ด ์ด๋ฏธ ๋ฐฐํฌ๋œ ์ƒํƒœ์—์„œ๋„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ํŽ˜์ด์ง€๋ฅผ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋กœ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ fallback ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜์ง€ ์•Š์€ ํŽ˜์ด์ง€์— ์ฒ˜์Œ ๋ฐฉ๋ฌธํ–ˆ์„ ๋•Œ ์„œ๋ฒ„๋Š” ์ฆ‰์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ getStaticProps๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ๋กœ์šด HTML ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ƒ…๋‹ˆ๋‹ค.

getStaticProps๋Š” ๋นŒ๋“œ ํƒ€์ž„์ด๋‚˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ '๋ฏธ๋ฆฌ' ์‹คํ–‰๋˜๋Š” ์„ฑ๊ฒฉ์„ ๊ฐ€์กŒ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ € ์ฃผ์†Œ์ฐฝ์— ์ž…๋ ฅํ•œ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ HTTP ํ—ค๋” ์ •๋ณด์—๋Š” ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. HTML์„ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘๋Š” ์‹œ์ ์—๋Š” ๋ˆ„๊ฐ€ ์–ด๋–ค ์š”์ฒญ์„ ๋ณด๋‚ผ์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด์ฃ . ์‚ฌ์šฉ์ž ๊ฐœ๊ฐœ์ธ์˜ ์š”์ฒญ ์ •๋ณด๊ฐ€ ๋ฐ˜๋“œ์‹œ ์š”๊ตฌ๋œ๋‹ค๋ฉด getStaticProps๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ•จ๊ป˜ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ SSR ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด์•ผ ํ•˜๊ฒ ์ฃ ?

1-4. CMS ๋ฐ์ดํ„ฐ ํŽ˜์นญ ํ™œ์šฉ โœ…

์•„๋ž˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด CMS์—์„œ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

js
// getStaticProps()์— ์˜ํ•ด ๋นŒ๋“œ ํƒ€์ž„์— posts ๋ฐ์ดํ„ฐ๊ฐ€ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค.
export default function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

// ์„œ๋ฒ„ ์ธก์˜ ๋นŒ๋“œ ํƒ€์ž„์— ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
// ํด๋ผ์ด์–ธํŠธ ์ธก(๋ธŒ๋ผ์šฐ์ €)์—์„œ๋Š” ํ˜ธ์ถœ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, 
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
export async function getStaticProps() {
  // ๊ฒŒ์‹œ๋ฌผ์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด ์™ธ๋ถ€ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  // ์–ด๋–ค ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // { props: { posts } }๋ฅผ ๋ฐ˜ํ™˜ํ•จ์œผ๋กœ์จ, Blog ์ปดํฌ๋„ŒํŠธ๋Š”
  // ๋นŒ๋“œ ํƒ€์ž„์— `posts`๋ฅผ prop์œผ๋กœ ์ „๋‹ฌ๋ฐ›๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  return {
    props: {
      posts,
    },
  }
}

1-5. ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์ฝ”๋“œ ์ง์ ‘ ์ž‘์„ฑ โœ…

getStaticProps์—์„œ๋Š” ์„œ๋น„์Šค์˜ ๋‚ด๋ถ€ API Route(/api/posts)๋ฅผ ๋‹ค์‹œ fetch๋กœ ํ˜ธ์ถœํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ์ด์œ ๋Š” ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค. getStaticProps ์ž์ฒด๊ฐ€ ์ด๋ฏธ ์„œ๋ฒ„ ํ™˜๊ฒฝ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‚ด๋ถ€ API๋ฅผ fetch๋กœ ํ˜ธ์ถœํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ด์ค‘์œผ๋กœ ๋ณด๋‚ด๋Š” ์ƒํ™ฉ์ด ๋ฉ๋‹ˆ๋‹ค.

pages/api/posts.js API Route๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด ๋ณด์ฃ .

js
export default async function handler(req, res) {
  const posts = await fetch("https://my-cms.com/posts").then(r => r.json());
  res.status(200).json(posts);
}

pages/index.js์˜ getStaticProps์—์„œ ๋‹ค์‹œ ์ด๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค๊ณ  ํ•ด๋ด…์‹œ๋‹ค.

js
export async function getStaticProps() {
  const res = await fetch("http://localhost:3000/api/posts");
  const posts = await res.json();

  return {
    props: { posts },
  };
}

export default function Home({ posts }) {
  return <div>{posts.length} posts</div>;
}

getStaticProps ์„œ๋ฒ„ ์ฝ”๋“œ์—์„œ ๋‚ด๋ถ€ api route๋กœ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ํ•œ ๋ฒˆ, /api/posts.js์—์„œ CMS๋กœ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ํ•œ ๋ฒˆ, ์ด ๋‘ ๋ฒˆ์˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ๋ณด์‹œ์ฃ .

js
// lib/load-posts.js (๊ณตํ†ต ๋กœ์ง)
export async function loadPosts() {
  // ์™ธ๋ถ€ CMS๋‚˜ DB์— ์ง์ ‘ ์ ‘๊ทผ (๋„คํŠธ์›Œํฌ 1๋‹จ๊ณ„)
  const res = await fetch('https://external-cms.com/posts')
  return await res.json()
}

// pages/blog.js
import { loadPosts } from '../lib/load-posts'

export async function getStaticProps() {
  // ๋‚ด๋ถ€ API๋ฅผ fetch ํ•˜์ง€ ์•Š๊ณ , ๋กœ์ง์„ ์ง์ ‘ ์‹คํ–‰ํ•˜์—ฌ ์ค‘๋ณต ๋„คํŠธ์›Œํฌ๋ฅผ ์ œ๊ฑฐํ•จ
  const posts = await loadPosts() 
  return { props: { posts } }
}

์ด ๊ฒฝ์šฐ์—๋Š” getStaticProps์—์„œ ๊ณง๋ฐ”๋กœ CMS๋กœ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ•œ ๋ฒˆ๋งŒ ๋ณด๋‚ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ getStaticProps๋Š” ๊ทธ ์ž์ฒด๊ฐ€ ์ด๋ฏธ ์„œ๋ฒ„์ด๋ฏ€๋กœ, ๋˜ ๋‹ค๋ฅธ ์„œ๋ฒ„ ํ†ต๋กœ์ธ API Route๋ฅผ ๊ฑฐ์น  ํ•„์š” ์—†์ด ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋กœ์ง์„ ์ง์ ‘ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์„ฑ๋Šฅ ์ตœ์ ํ™”์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

1-6. HTML ๋ฐ JSON ์ •์  ์ƒ์„ฑ ๋งค์ปค๋‹ˆ์ฆ˜ โœ…

getStaticProps๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํŽ˜์ด์ง€๊ฐ€ ๋นŒ๋“œ ํƒ€์ž„์— Pre-Rendering ๋  ๋•Œ, Next.js๋Š” ํŽ˜์ด์ง€ HTML ํŒŒ์ผ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ getStaticProps ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์€ JSON ํŒŒ์ผ๋„ ํ•จ๊ป˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

JSON ํŒŒ์ผ์€ next/link๋‚˜ next/router๋ฅผ ํ†ตํ•œ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋ผ์šฐํŒ…์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. getStaticProps๋กœ Pre-Rendering ๋œ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•  ๋•Œ, Next.js๋Š” ๋นŒ๋“œ ํƒ€์ž„์— ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ JSON ํŒŒ์ผ์„ ๊ฐ€์ ธ์™€ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ์—๋Š” ์ด๋ฏธ ์ƒ์„ฑ๋œ JSON๋งŒ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ getStaticProps๊ฐ€ ๋‹ค์‹œ ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ฆ๋ถ„ ์ •์  ์žฌ์ƒ์„ฑ(ISR)์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜์— ํ•„์š”ํ•œ JSON์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ getStaticProps๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋™์ผํ•œ ํŽ˜์ด์ง€์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ์š”์ฒญ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์˜๋„๋œ ๋™์ž‘์ด๋ฉฐ ์ตœ์ข… ์‚ฌ์šฉ์ž ์„ฑ๋Šฅ์—๋Š” ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

1-7. getStaticProps ์‚ฌ์šฉ ๊ฐ€๋Šฅ ๋ฒ”์œ„ โœ…

getStaticProps ํ•จ์ˆ˜๋Š” ์˜ค์ง 'ํŽ˜์ด์ง€ ํŒŒ์ผ'์—์„œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. getStaticProps๋Š” pages ํด๋” ๋‚ด์— ์žˆ๋Š” ํŽ˜์ด์ง€์šฉ ํŒŒ์ผ์—์„œ๋งŒ export ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ UI ์ปดํฌ๋„ŒํŠธ๋‚˜ ๋ชจ๋“  ํŽ˜์ด์ง€์˜ ๊ณตํ†ต ์„ค์ •์„ ๋‹ด๋‹นํ•˜๋Š” _app.js, _document.js ๊ฐ™์€ ํŠน์ˆ˜ ํŒŒ์ผ์—์„œ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๊ฐ€ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๊ธฐ ์ „, ํ•ด๋‹น ํŽ˜์ด์ง€์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ์™„๋ฒฝํ•˜๊ฒŒ ์ค€๋น„(Pre-Fetching) ํ•ด์•ผ ํ•˜๋Š” ๊ตฌ์กฐ์  ์ด์œ  ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋˜ํ•œ, ๋ฐ˜๋“œ์‹œ '๋…๋ฆฝ๋œ ํ•จ์ˆ˜'๋กœ ๋‚ด๋ณด๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. export async function getStaticProps() { ... }์™€ ๊ฐ™์ด ๋…๋ฆฝ์ ์ธ ์ด๋ฆ„์„ ๊ฐ€์ง„ ํ•จ์ˆ˜๋กœ ๋‚ด๋ณด๋‚ด์•ผ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ๊ฐ์ฒด์˜ ์†์„ฑ์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด Next.js๊ฐ€ ์ด๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๋ฏ€๋กœ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.


2. getStaticPaths ๐ŸŽฏ

2-1. getStaticPaths ์‚ฌ์šฉ โœ…

๋™์  ๊ฒฝ๋กœ๊ฐ€ ์žˆ๋Š” ํŽ˜์ด์ง€์—์„œ getStaticProps๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, ์ •์ ์œผ๋กœ ์ƒ์„ฑ๋  ๊ฒฝ๋กœ ๋ชฉ๋ก์„ ๋ฏธ๋ฆฌ ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋™์  ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํŽ˜์ด์ง€์—์„œ getStaticPaths ํ•จ์ˆ˜๋ฅผ export ํ•˜๋ฉด, Next.js๋Š” getStaticPaths์— ์ง€์ •๋œ ๋ชจ๋“  ๊ฒฝ๋กœ๋ฅผ ์ •์ ์œผ๋กœ Pre-Rendering ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ ํ˜•ํƒœ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

js
export async function getStaticPaths() {
  return {
    paths: [
      {
        params: {
          name: 'next.js',
        },
      },
    ],
    fallback: true,
  };
}

export async function getStaticProps() {
  const res = await fetch('https://api.github.com/repos/vercel/next.js');
  const repo = await res.json();
  
  return { props: { repo } };
}

export default function Page({ repo }) {
  return repo.stargazers_count;
}

2-2. getStaticPaths ์‚ฌ์šฉ ์‹œ์  โœ…

getStaticPaths๋Š” ๋ณธ์งˆ์ ์œผ๋กœ getStaticProps์™€ ๋™์ผํ•œ ์†Œ์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. CMS๋‚˜ DB, ํŒŒ์ผ ์‹œ์Šคํ…œ ๋“ฑ์—์„œ์š”. ํ•˜์ง€๋งŒ ๋ชฉ์ ์—์„œ๋Š” ๊ฒฐ์ •์ ์ธ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. about.js์™€ ๊ฐ™์€ ๊ณ ์ •๋œ ๊ฒฝ๋กœ์˜ ํŽ˜์ด์ง€์™€ ๋‹ฌ๋ฆฌ, [id].js์™€ ๊ฐ™์€ ๋™์  ๊ฒฝ๋กœ๋Š” ๋นŒ๋“œ ์‹œ์ ์— Next.js๊ฐ€ ๋„๋Œ€์ฒด ๋ช‡ ๊ฐœ์˜ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ• ์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด ํƒ€์ด๋ฐ์— getStaticPaths๊ฐ€ ๊ฐœ์ž…ํ•˜์—ฌ "1๋ฒˆ๋ถ€ํ„ฐ 100๋ฒˆ๊นŒ์ง€์˜ ๊ฒŒ์‹œ๊ธ€ ์ฃผ์†Œ๊ฐ€ ์žˆ์œผ๋‹ˆ, ์ด ๋ชฉ๋ก์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฏธ๋ฆฌ ์ง€๋„๋ฅผ ๊ทธ๋ ค๋‘์–ด๋ผ!"๋ผ๋Š” ์‹์˜ ๋ช…๋ น์„ ๋‚ด๋ฆฝ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ์˜ ๋‚ด์šฉ์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์ „์— ์ฃผ์†Œ ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•์ • ์ง“๊ธฐ ์œ„ํ•œ ์ผ์ข…์˜ ์‚ฌ์ „ ์ž‘์—…์ธ ์…ˆ์ž…๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ getStaticPaths๋Š” SEO์™€ ์••๋„์ ์ธ ์„ฑ๋Šฅ์ด ๋™์‹œ์— ์š”๊ตฌ๋˜๋ฉด์„œ๋„, ๊ทธ ๋Œ€์ƒ ํŽ˜์ด์ง€๊ฐ€ ๊ฐ€๋ณ€์ ์ธ ๋™์  ๋ผ์šฐํŒ… ๋ฐฉ์‹์„ ๋”ฐ๋ผ์•ผ ํ•  ๋•Œ ํ•„์ˆ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ œํ’ˆ ์ƒ์„ธ๋‚˜ ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ ๋“ฑ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ ์ข…์†๋˜์ง€ ์•Š๋Š” ๊ณต์šฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ getStaticPaths๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์€ ์ˆ˜์ฒœ, ์ˆ˜๋งŒ ๊ฐœ์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ์‚ฌ์šฉ์ž ์š”์ฒญ์ด ๋“ค์–ด์˜ค๊ธฐ ์ „์— ๋ฏธ๋ฆฌ '์ค€๋น„ํ•œ๋‹ค'๋ผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์–ด๋–ค ์ƒ์„ธ ํŽ˜์ด์ง€์— ์ ‘์†ํ•˜๋”๋ผ๋„ ์„œ๋ฒ„์˜ ์—ฐ์‚ฐ์„ ๊ธฐ๋‹ค๋ฆด ํ•„์š” ์—†์ด, ์ด๋ฏธ ์ „ ์„ธ๊ณ„ CDN์— ๋ฐฐํฌ๋œ ์™„์„ฑํ˜• ํŽ˜์ด์ง€๋ฅผ ๋น ๋ฅธ ์†๋„๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

2-3. getStaticProps์™€์˜ ์ƒํ˜ธ์ž‘์šฉ โœ…

getStaticPaths๋Š” ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์˜ค์ง ๋นŒ๋“œ ์‹œ์ ์— ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ๊ฒƒ์€ getStaticProps์™€์˜ ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค.

๊ฐ€์žฅ ๋จผ์ € next build ์ค‘์— getStaticPaths๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ getStaticPaths๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๋ชจ๋“  paths์— ๋Œ€ํ•ด getStaticProps๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ fallback์„ true๋กœ ์„ค์ •ํ•˜๋ฉด ๋นŒ๋“œ ํƒ€์ž„์— ์ƒ์„ฑ๋˜์ง€ ์•Š์€ ํŽ˜์ด์ง€ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ getStaticProps๊ฐ€ ์‹คํ–‰๋˜๊ณ , blocking์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ์ดˆ๊ธฐ ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚˜๊ธฐ ์ง์ „์— getStaticProps๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค.

getStaticPaths์™€ getStaticProps ๋ชจ๋‘ ์„œ๋ฒ„์™€ ๋นŒ๋“œ ํƒ€์ž„์ด๋ผ๋Š” ํฐ ํ‹€์—์„œ ๋ณด๋ฉด ์™„์ „ํžˆ ๋™์ผํ•œ ๊ถค๋ฅผ ๊ทธ๋ฆฌ๋ฉฐ ์›€์ง์ž…๋‹ˆ๋‹ค. ํ•ต์‹ฌ์€ SSG์ž…๋‹ˆ๋‹ค. SSG์˜ ๋ชฉํ‘œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•˜๊ธฐ ์ „์— ๋ชจ๋“  ์ค€๋น„๋ฅผ ๋๋‚ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‘ ํ•จ์ˆ˜ ๋ชจ๋‘ ๋นŒ๋“œ ์‹œ์ ์— ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ์ž‘์—…์„ ๋๋‚ด๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋น„์Šค์— ์ ‘์†ํ–ˆ์„ ๋•Œ์—๋Š” ์„œ๋ฒ„๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ฐพ์•„๋ณผ ํ•„์š” ์—†์ด ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ HTML๊ณผ JSON ํŒŒ์ผ๋งŒ ํˆญ ๋˜์ ธ์ฃผ๋ฉด ๋๋‚ฉ๋‹ˆ๋‹ค.

getStaticPaths์™€ getStaticProps์˜ ๋ฏธ์„ธํ•œ ์ฐจ์ด๋Š” getServerSideProps์™€ Incremental Static Regeneration ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š”๋ฐ์š”, ์ด์— ๋Œ€ํ•ด์„œ๋Š” ํ›„์ˆ ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

2-4. ์˜จ๋””๋งจ๋“œ(On-demand) ๊ฒฝ๋กœ ์ƒ์„ฑ โœ…

์ด๋ฏธ ์–ธ๊ธ‰ํ–ˆ์ง€๋งŒ, getStaticPaths์˜ fallback ์˜ต์…˜์„ ํ™œ์šฉํ•˜๋ฉด ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๋นŒ๋“œ ์‹œ์ ์— ๋งŒ๋“œ๋Š” ๋Œ€์‹ , ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋Š” ์‹œ์ (On-demand)์— ์ƒ์„ฑ๋˜๋„๋ก ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋นŒ๋“œ ์‹œ์ ์— ๋„ˆ๋ฌด ๋งŽ์€ ํŽ˜์ด์ง€๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋นŒ๋“œ ์†๋„๊ฐ€ ๋А๋ ค์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

paths์— ๋นˆ ๋ฐฐ์—ด([])์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ชจ๋“  ํŽ˜์ด์ง€์˜ ์ƒ์„ฑ์„ On-demand ๋ฐฉ์‹์œผ๋กœ ๋ฏธ๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์—ฌ๋Ÿฌ ํ™˜๊ฒฝ์— ๋ฐฐํฌํ•  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ •์  ํŽ˜์ด์ง€๊ฐ€ ์ˆ˜๋ฐฑ, ์ˆ˜์ฒœ ๊ฐœ์— ๋‹ฌํ•˜๋Š” ์‚ฌ์ดํŠธ์˜ ๊ฒฝ์šฐ ํ”„๋ฆฌ๋ทฐ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ On-demand๋กœ ์ƒ์„ฑํ•˜๊ฒŒ ํ•˜์—ฌ ๋นŒ๋“œ ์†๋„๋ฅผ ๋†’์ด๊ณ , ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋งŒ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

js
export async function getStaticPaths() {
  // ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ์‹œ ์ •์  ํŽ˜์ด์ง€ ๋ฏธ๋ฆฌ ์ƒ์„ฑ ์•ˆ ํ•จ
  // (๋นŒ๋“œ๋Š” ๋นจ๋ผ์ง€์ง€๋งŒ, ์ฒซ ์ ‘์† ์‹œ ๋กœ๋”ฉ ์†๋„๋Š” ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์Œ)
  if (process.env.SKIP_BUILD_STATIC_GENERATION) {
    return {
      paths: [],
      fallback: 'blocking',
    }
  }

  // ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•  ๊ฒฝ๋กœ(ID) ๋ชฉ๋ก ์ƒ์„ฑ
  // ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜์—ฌ ์†๋„ ์ตœ์ ํ™”
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒฝ๋กœ๋Š” 404 ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  return { paths, fallback: false }
}

3. getServerSideProps ๐ŸŽฏ

3-1. getServerSideProps ์‚ฌ์šฉ โœ…

ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ์—์„œ getServerSideProps๋ฅผ export ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

js
export async function getServerSideProps() {
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  const repo = await res.json()
  return { props: { repo } }
}
 
export default function Page({ repo }) {
  return (
    <main>
      <p>{repo.stargazers_count}</p>
    </main>
  )
}

getServerSideProps๋Š” ๊ฐœ์ธํ™”๋œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋‚˜ ์œ„์น˜ ์ •๋ณด์ฒ˜๋Ÿผ ์š”์ฒญ ์‹œ์ ์—๋งŒ ์•Œ ์ˆ˜ ์žˆ๋Š” ์ •๋ณด์— ์˜์กดํ•˜๋Š” ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋ง ํ•ด์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์š”์ฒญ ์‹œ์ ์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ํ•„์š”๊ฐ€ ์—†๊ฑฐ๋‚˜, ๋ฐ์ดํ„ฐ์™€ Pre-Rendering ๋œ HTML์„ ์บ์‹ฑ ํ•˜๋Š” ์ƒํ™ฉ์ด ๋” ์ ์ ˆํ•˜๋‹ค๋ฉด getServerSideProps๊ฐ€ ์•„๋‹ˆ๋ผ getStaticProps๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹๊ฒ ์ฃ ?

3-2. Edge Cases โœ…

getServerSideProps๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ƒํ™ฉ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ

๋‚ด๋ถ€์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ ํŠน์ • ์กฐ๊ฑด์— ๋”ฐ๋ผ ์‚ฌ์šฉ์ž๋ฅผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ๋ณด๋‚ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ notFound๋‚˜ redirect ์†์„ฑ์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค.

js
export async function getServerSideProps(context) {
  const res = await fetch(`https://api.example.com/data/${context.params.id}`)
  
  // ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ 404 ํŽ˜์ด์ง€๋กœ
  if (!res.ok) {
    return {
      notFound: true,
    }
  }
  
  const data = await res.json()
  
  // ํŠน์ • ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  if (!data.published) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }
  
  return {
    props: { data },
  }
}

Context ๊ฐ์ฒด ํ™œ์šฉ

getServerSideProps๋Š” ๋งค ์š”์ฒญ ์‹œ ๋™์ž‘ํ•˜๊ธฐ์—, ์š”์ฒญ ๊ด€๋ จ ์ •๋ณด๋ฅผ ๋‹ด์€ context ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์ด getStaticProps์™€์˜ ๊ฒฐ์ •์ ์ธ ์ฐจ์ด์ ์ž…๋‹ˆ๋‹ค.

js
export async function getServerSideProps(context) {
  // context ๊ฐ์ฒด์—๋Š” ๋‹ค์–‘ํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ์Šต๋‹ˆ๋‹ค
  const {
    params,       
    req,          
    res,          
    query,        
    preview,      
    previewData,  
    resolvedUrl,  
    locale,       
    locales,      
  } = context
  
  // ์ฟ ํ‚ค๋‚˜ ํ—ค๋” ์ •๋ณด ์ ‘๊ทผ ๊ฐ€๋Šฅ
  const token = req.cookies.auth_token
  const userAgent = req.headers['user-agent']
  
  return {
    props: {
      userAgent,
    },
  }
}

์บ์‹ฑ ๋™์ž‘ ์ œ์–ด

๋งˆ์ง€๋ง‰์œผ๋กœ, getServerSideProps๋Š” ๋งค ์š”์ฒญ๋งˆ๋‹ค ์‹คํ–‰๋˜์ง€๋งŒ Cache-Control ํ—ค๋”๋ฅผ ํ†ตํ•ด ์บ์‹ฑ ์ „๋žต์„ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์กฐ์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

js
export async function getServerSideProps({ res }) {
  // 60์ดˆ ๋™์•ˆ ์บ์‹œํ•˜๊ณ , ๊ทธ ์ดํ›„ 10์ดˆ ๋™์•ˆ์€ stale ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ
  // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์žฌ๊ฒ€์ฆ(stale-while-revalidate)
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=60, stale-while-revalidate=10'
  )
  
  const data = await fetch('https://api.example.com/data')
  const json = await data.json()
  
  return {
    props: { data: json },
  }
}

4. Incremental Static Regeneration(ISR) ๐ŸŽฏ

ISR์€ ์šฐ๋ฆฌ๊ฐ€ ์•ž์„œ ํ•™์Šตํ–ˆ๋“ฏ, SSG์˜ ์„ฑ๋Šฅ๊ณผ SSR์˜ ์œ ์—ฐ์„ฑ์„ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ „๋žต์ž…๋‹ˆ๋‹ค. ๋นŒ๋“œ ์‹œ์ ์— ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋Œ€์‹ , ์ผ๋ถ€ ํŽ˜์ด์ง€๋งŒ ๋ฏธ๋ฆฌ ๋งŒ๋“ค๊ณ  ๋‚˜๋จธ์ง€ ํŽ˜์ด์ง€๋Š” ์š”์ฒญ์— ๋”ฐ๋ผ ์ƒ์„ฑํ•˜๋ฉด์„œ๋„, ํ•œ ๋ฒˆ ์ƒ์„ฑ๋œ ํŽ˜์ด์ง€๋Š” ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

4-1. On-Demand Revalidation โœ…

ISR์€ revalidate ์˜ต์…˜์œผ๋กœ ์„ค์ •ํ•œ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํŽ˜์ด์ง€๋ฅผ ์žฌ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

js
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const post = await getPostBySlug(params.slug)
  
  return {
    props: { post },
    // 60์ดˆ๋งˆ๋‹ค ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํŽ˜์ด์ง€ ์žฌ์ƒ์„ฑ
    revalidate: 60,
  }
}

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: 'blocking',
  }
}

ํ•˜์ง€๋งŒ ๋•Œ๋กœ๋Š” ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚œ ํ›„๊ฐ€ ์•„๋‹ˆ๋ผ, ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š” ์ฆ‰์‹œ ํŽ˜์ด์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋ น ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋ฅผ ์ˆ˜์ •ํ•˜์ž๋งˆ์ž ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ˜์˜๋˜๊ธฐ๋ฅผ ์›ํ•  ์ˆ˜ ์žˆ์ฃ . ์ฆ‰ On-Demand์— ์˜ํ•œ Revalidation์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

On-Demand Revalidation์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” getStaticProps์— revalidate ์‹œ๊ฐ„์„ ๋ช…์‹œํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ„ ์„ค์ •์„ ์ƒ๋žตํ•˜๋ฉด ๊ธฐ๋ณธ๊ฐ’์ธ false๊ฐ€ ๋˜๋ฉฐ, ์˜ค์ง revalidate()๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋งŒ ํŽ˜์ด์ง€๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

js
// pages/api/revalidate.js
export default async function handler(req, res) {
  // ๋ณด์•ˆ์„ ์œ„ํ•œ ํ† ํฐ ๊ฒ€์ฆ
  if (req.query.secret !== process.env.REVALIDATE_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }
  
  try {
    // ํŠน์ • ๊ฒฝ๋กœ์˜ ํŽ˜์ด์ง€๋ฅผ ์ฆ‰์‹œ ์žฌ์ƒ์„ฑ
    await res.revalidate('/blog/my-post')
    
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).send('Error revalidating')
  }
}

getServerSideProps๋Š” ๋งค ์š”์ฒญ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๊ธฐ์— ๋ณ„๋„์˜ ์บ์‹ฑ ๋กœ์ง์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ISR์€ ๋นŒ๋“œ ์‹œ์ ์— ์ด๋ฏธ ์ƒ์„ฑ๋œ ์ •์  ์ž์‚ฐ์„ ์„œ๋น™ํ•˜๋Š” ๋ฐฉ์‹์ด๊ธฐ์—, ๋ฌผ๋ฆฌ์ ์ธ ํŒŒ์ผ ๊ต์ฒด๋ฅผ ์œ„ํ•œ ๋ณ„๋„์˜ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž์˜ ํŽ˜์ด์ง€ ๋ฐฉ๋ฌธ ์‹œ์ ์ด ์•„๋‹ˆ๋ผ CMS์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์‹œ์ ์ฒ˜๋Ÿผ, ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์„œ๋ฒ„์— '์ •์  ํŒŒ์ผ ์žฌ์ƒ์„ฑ'์„ ๋ช…๋ นํ•ด์•ผ ํ•˜๋ฏ€๋กœ, res.revalidate()๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋…๋ฆฝ์ ์ธ API ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์š”๊ตฌ๋ฉ๋‹ˆ๋‹ค.

ํ•œ๋งˆ๋””๋กœ, ๋ฐ์ดํ„ฐ ์š”์ฒญ๊ณผ ์บ์‹œ ๋ฌดํšจํ™”๋Š” ์‹คํ–‰ ์ฃผ์ฒด์™€ ์‹œ์ ์ด ์™„์ „ํžˆ ๋‹ค๋ฅธ ๋กœ์ง์ด๊ธฐ์— API Route๋ฅผ ํ†ตํ•ด ์ œ์–ด๊ถŒ์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

4-2. Error handling and revalidation โœ…

ISR ๋ฐฉ์‹์„ ๋”ฐ๋ฅผ ๋•Œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ ์ƒํ™ฉ๋“ค์„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

Revalidation ์‹คํŒจ

getStaticProps ๋‚ด๋ถ€์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„, ์ด๋ฏธ ์ƒ์„ฑ๋œ ํŽ˜์ด์ง€๋Š” ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. Revalidation ๊ณผ์ • ์ค‘ getStaticProps ๋‚ด์—์„œ ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„, Next.js๋Š” ์ฆ‰์‹œ ์—๋Ÿฌ ํ™”๋ฉด์„ ๋„์šฐ๋Š” ๋Œ€์‹  ๊ธฐ์กด์— ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ๋˜ ์ตœ์‹  ์ •์  ํŽ˜์ด์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์ผ์‹œ์ ์œผ๋กœ ๋ถˆ์•ˆ์ •ํ•œ ์ƒํ™ฉ์—์„œ๋„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ตœ์†Œํ•œ์˜ ์ •์ƒ์ ์ธ ํ™”๋ฉด์„ ๋ณด์žฅํ•˜๋ฉฐ, ์‹œ์Šคํ…œ์€ ๋‹ค์Œ ์žฌ๊ฒ€์ฆ ์ฃผ๊ธฐ์— ๋งž์ถฐ ๋‹ค์‹œ ํŽ˜์ด์ง€ ๊ฐฑ์‹ ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

js
export async function getStaticProps() {
  try {
    const data = await fetchDataFromAPI()
    
    return {
      props: { data },
      revalidate: 60,
    }
  } catch (error) {
    // ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์ด์ „์— ์ƒ์„ฑ๋œ ํŽ˜์ด์ง€๋Š” ๊ณ„์† ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค
    // ๋‹ค์Œ ์žฌ๊ฒ€์ฆ ์‹œ์ ์— ๋‹ค์‹œ ์‹œ๋„๋ฉ๋‹ˆ๋‹ค
    console.error('Revalidation failed:', error)
    
    // ์ด์ „ ํŽ˜์ด์ง€๋ฅผ ์œ ์ง€ํ•˜๋ ค๋ฉด throwํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
    // ๋งŒ์•ฝ ์—๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ๋‹ค๋ฉด throw error
    return {
      notFound: true, // ๋˜๋Š” ์—๋Ÿฌ props ์ „๋‹ฌ
    }
  }
}

์กฐ๊ฑด๋ถ€ Revalidation

๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ๋™์ผํ•œ ์ฃผ๊ธฐ๋กœ Revalidationํ•  ํ•„์š”๋Š” ์—†๊ฒ ์ฃ ? ๋ฐ์ดํ„ฐ์˜ ํŠน์„ฑ์— ๋”ฐ๋ผ ์„œ๋กœ ๋‹ค๋ฅธ revalidate ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

js
export async function getStaticProps({ params }) {
  const post = await getPostBySlug(params.slug)
  
  // ์ตœ๊ทผ ๊ฒŒ์‹œ๋ฌผ์€ ์ž์ฃผ ์—…๋ฐ์ดํŠธ, ์˜ค๋ž˜๋œ ๊ฒŒ์‹œ๋ฌผ์€ ๋œ ์—…๋ฐ์ดํŠธ
  const isRecent = Date.now() - new Date(post.createdAt).getTime() < 7 * 24 * 60 * 60 * 1000 // 7์ผ
  
  return {
    props: { post },
    // ์ตœ๊ทผ ๊ธ€์€ 1๋ถ„, ์˜ค๋ž˜๋œ ๊ธ€์€ 1์‹œ๊ฐ„
    revalidate: isRecent ? 60 : 3600, 
  }
}

Revalidation ์ค‘๋ณต ๋ฐฉ์ง€

๋™์ผํ•œ ํŽ˜์ด์ง€์— ๋Œ€ํ•ด ๋™์‹œ์— ์—ฌ๋Ÿฌ Revalidation ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Next.js๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ค‘๋ณต ์‹คํ–‰์„ ๋ฐฉ์ง€ํ•˜์ง€๋งŒ, API ํ˜ธ์ถœ์„ ์ตœ์ ํ™”ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ง์ ‘ ์ œ์–ดํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

js
// lib/cache.js
const revalidationCache = new Map()

export async function fetchWithDedupe(key, fetcher, ttl = 5000) {
  const cached = revalidationCache.get(key)
  
  // ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์ด ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.promise
  }
  
  const promise = fetcher()
  revalidationCache.set(key, {
    promise,
    timestamp: Date.now(),
  })
  
  // ์บ์‹œ ์ •๋ฆฌ
  setTimeout(() => revalidationCache.delete(key), ttl)
  
  return promise
}

// pages/blog/[slug].js
import { fetchWithDedupe } from '@/lib/cache'

export async function getStaticProps({ params }) {
  const post = await fetchWithDedupe(
    `post-${params.slug}`,
    () => getPostBySlug(params.slug)
  )
  
  return {
    props: { post },
    revalidate: 60,
  }
}

5. Client-Side Fetching ๐ŸŽฏ

์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ HTML์„ ๋ฏธ๋ฆฌ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์ด ๋Šฅ์‚ฌ๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ๋” ์ ์ ˆํ•œ ๊ฒฝ์šฐ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

5-1. Client-Side data fetching with useEffect โœ…

SEO๊ฐ€ ์ค‘์š”ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ํŽ˜์ด์ง€๋ฅผ Pre-Rendering ํ•  ํ•„์š”๊ฐ€ ์—†๊ฑฐ๋‚˜, ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž์ฃผ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๊ฒฝ์šฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์‹ค ์ง€๊ธˆ๊นŒ์ง€ ์ž‘์„ฑํ•ด์˜จ ๋ฐฉ์‹์ด๋„ ํ•ฉ๋‹ˆ๋‹ค.

js
import { useState, useEffect } from 'react'
 
function Profile() {
  const [data, setData] = useState(null)
  const [isLoading, setLoading] = useState(true)
 
  useEffect(() => {
    fetch('/api/profile-data')
      .then((res) => res.json())
      .then((data) => {
        setData(data)
        setLoading(false)
      })
  }, [])
 
  if (isLoading) return <p>Loading...</p>
  if (!data) return <p>No profile data</p>
 
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.bio}</p>
    </div>
  )
}

5-2. Client-Side data fetching with SWR โœ…

useEffect๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์ง€๋งŒ, ๋งค๋ฒˆ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•˜๊ณ , ์บ์‹ฑ์ด๋‚˜ ์žฌ๊ฒ€์ฆ ๋“ฑ์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ธฐ๋Š” ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ด๋Ÿด ๋•Œ SWR์ด๋‚˜ Tanstack Query์™€ ๊ฐ™์€ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Tanstack Query๋Š” ํ‰์†Œ์— ์ž์ฃผ ์‚ฌ์šฉํ–ˆ์œผ๋‹ˆ SWR ์ฝ”๋“œ๋งŒ ๊ฐ€๋ณ๊ฒŒ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

SWR - ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

js
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

export default function Profile() {
  const { data, error, isLoading } = useSWR('/api/user/profile', fetcher)

  if (error) return <div>Failed to load</div>
  if (isLoading) return <div>Loading...</div>

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.bio}</p>
    </div>
  )
}

SWR - Mutation๊ณผ Optimistic UI

js
import useSWR, { useSWRConfig } from 'swr'

export default function TodoList() {
  const { data: todos } = useSWR('/api/todos', fetcher)
  const { mutate } = useSWRConfig()

  async function addTodo(title) {
    // Optimistic UI ์—…๋ฐ์ดํŠธ
    mutate(
      '/api/todos',
      [...todos, { id: Date.now(), title, completed: false }],
      false // ์žฌ๊ฒ€์ฆ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
    )

    // ์‹ค์ œ API ํ˜ธ์ถœ
    await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ title }),
    })

    // ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋กœ ์žฌ๊ฒ€์ฆ
    mutate('/api/todos')
  }

  return (
    <div>
      {todos?.map((todo) => (
        <div key={todo.id}>{todo.title}</div>
      ))}
      <button onClick={() => addTodo('์ƒˆ ํ•  ์ผ')}>์ถ”๊ฐ€</button>
    </div>
  )
}

6. Outro ๐ŸŽฏ

Next.js Pages Router์˜ Data Fetching์€ ํŽ˜์ด์ง€์˜ ์„ฑ๊ฒฉ์— ๋”ฐ๋ผ ์ตœ์ ์˜ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•œ ๋„ค ๊ฐ€์ง€ ์ „๋žต์œผ๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ๋™์ผํ•œ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๋Š” SSG๋Š” getStaticProps์™€ getStaticPaths๋ฅผ ํ†ตํ•ด ๋นŒ๋“œ ํƒ€์ž„์— ์ •์  ์ž์‚ฐ์„ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜์—ฌ ์••๋„์ ์ธ ์„ฑ๋Šฅ๊ณผ SEO๋ฅผ ๋ณด์žฅํ•˜๋ฉฐ, ๊ฐœ์ธํ™”๋œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋Š” getServerSideProps๋ฅผ ํ™œ์šฉํ•ด ๋งค ์š”์ฒญ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์ด์— ๋”ํ•ด ISR์€ ์ •์  ํŽ˜์ด์ง€ ๋ฐฐํฌ ํ›„์—๋„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐฑ์‹ ํ•˜์—ฌ SSG์˜ ํšจ์œจ์„ฑ๊ณผ SSR์˜ ์œ ์—ฐ์„ฑ์„ ๋™์‹œ์— ํ™•๋ณดํ•˜๋ฉฐ, ๋Œ€์‹œ๋ณด๋“œ์ฒ˜๋Ÿผ ์‹ค์‹œ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ์ด ์ค‘์š”ํ•œ ์˜์—ญ์€ SWR์ด๋‚˜ Tanstack Query๋ฅผ ํ™œ์šฉํ•œ CSR ๋ฐฉ์‹์œผ๋กœ ๋ณด์™„ํ•˜์—ฌ ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

More to read

Amazon VPC

Amazon VPC Architecture ์ดํ•ดํ•˜๊ธฐ

์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋ฅผ ๊ธฐํšํ•˜๋ฉฐ, ๊ฐœ๋ฐœ์—์„œ ๋ฌด์—‡์„ ๊ฐ€์žฅ ๋จผ์ € ๊ณ ๋ฏผํ•ด์•ผ ํ•˜๋Š”์ง€ ๋‹ค์‹œ ๋Œ์•„๋ณด๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.ํ•œ๋•Œ๋Š” ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋ชจ๋“  ์„ค๊ณ„์˜ ์ถœ๋ฐœ์ ์ด๋ผ๊ณ  ๋ฏฟ์—ˆ์Šต๋‹ˆ๋‹ค. ์œ ์ €๊ฐ€ ๋ฌด์—‡์„ ๋ณด๊ณ , ์–ด๋–ค ํ๋ฆ„์—์„œ ๋จธ๋ฌด๋ฅด๊ณ  ์ดํƒˆํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์ดํ•ด ์—†์ด ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ ๋‹ค๋Š” ๊ฑด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ

'์›์‚ฌ์ดํŠธ'

ํ”„๋ก ํŠธ์—”๋“œ ๊ด€์ ์œผ๋กœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ดํ•ดํ•˜๊ธฐ

์˜ค๋žœ๋งŒ์— ๋ฐฉ๋ฒ•๋ก ์— ๊ด€ํ•œ ๊ธ€์„ ์“ฐ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ตœ๊ทผ ์ƒํ™ฉ์€ ์ด๋ ‡์Šต๋‹ˆ๋‹ค. SSAFY์—์„œ๋Š” ํ•˜๋ฃจ์— ์—„์ฒญ๋‚œ ์–‘์˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ฌธ์ œ๋“ค์„ ๊ณผ์ œ๋กœ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ, '๊ตฌํ˜„๋ ฅ'์ด ๋งค์šฐ ๋–จ์–ด์ง„๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์™„์ „ํžˆ ์–ด๋ ค์šด ๋ฌธ์ œ๋ผ๋ฉด '์•„์‰ฌ์›€'์ด๋ผ๋Š” ๊ฐ์ •์กฐ์ฐจ ๋А๋ผ์ง€

Subnet

VPC ์„ค๊ณ„์˜ ์‹œ์ž‘: IP์™€ Subnet

๋ฐ˜๋ณต๋˜๋Š” ๋ฃจํ‹ด ์†์—์„œ ์–ป์€ ์•ˆ์ •๊ฐ์„ ๋ฐœํŒ ์‚ผ์•„, ์ด์ œ๋Š” ๊ธฐ์ˆ ์  ์ŠคํŽ™ํŠธ๋Ÿผ์„ ๋„“ํžˆ๊ธฐ ์œ„ํ•œ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์— ์ฐฉ์ˆ˜ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์˜ ๋ชฉํ‘œ๋Š” ๋‹จ์ˆœํ•œ ํฌํŠธํด๋ฆฌ์˜ค ๊ตฌ์ถ•์„ ๋„˜์–ด, ์‹ค์ œ ์„œ๋น„์Šค ์ˆ˜์ค€์˜ ๋ธ”๋กœ๊ทธ ์‹œ์Šคํ…œ ๊ตฌํ˜„๊ณผ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ ์ ์šฉ ๋“ฑ ์‹ค๋ฌด์— ๊ฐ€๊นŒ์šด ์—ญ๋Ÿ‰์„ ํ•œ ๋‹จ๊ณ„