λΈλ‘κ·Έλ₯Ό Next.js + Cloud Run νκ²½μμ μ΄μνλ©΄μ λ λλ§ μ λ΅μ SSR β ISR/SSG β SSR λ‘ λ°κΎΈλ κ³Όμ μ κ²ͺμμ΅λλ€. μ²μμλ λ¨μνκ² SSR λ‘ μ΄μνκ³ , μ΄νμ μ±λ₯ κ°μ μ μν΄ ISR λ° SSG λ₯Ό μ μ©ν΄λ΄€μ§λ§, Cloud Run μ νΉμ±κ³Ό λ§μ§ μμ κ²°κ΅ λ€μ SSR λ‘ λμμ€κ² λμ΅λλ€. μ΄ κ³Όμ μμ λ§μ£ΌμΉ λ¬Έμ λ€κ³Ό μ΄μ λ€μ μ 리ν΄λ³΄λ €κ³ ν©λλ€.
본격μ μΌλ‘ μ€λͺ νκΈ° μ μ μ΄λ² μ¬μ μμ λ€λ£¬ λ λλ§ λ°©μλ€μ κ°λ¨ν μ 리ν©λλ€.
μμ²μ΄ λ€μ΄μ¬ λλ§λ€ μλ²μμ HTML μ μμ±ν©λλ€. Next.js App Router μμλ dynamic = "force-dynamic" μΌλ‘ μ€μ νκ±°λ, λ°μ΄ν° νμΉμ cache: "no-store" λ₯Ό μ¬μ©νλ©΄ λ©λλ€.
λΉλ μμ μ νμ΄μ§λ₯Ό μ μ μΌλ‘ μμ±νκ³ , μ€μ ν μ£ΌκΈ°(revalidate) λ§λ€ λλ μ¨λ맨λλ‘ μ¬μμ±ν©λλ€.
λΉλ μμ μ λͺ¨λ νμ΄μ§λ₯Ό μ μ μΌλ‘ μμ±ν©λλ€. Next.js μμ generateStaticParams μ revalidate = false λ₯Ό ν¨κ» μ¬μ©νλ λ°©μμ
λλ€.
λΈλ‘κ·Έ μ΄κΈ° κ΅¬μ± λΉμ λ λλ§ λ°©μμ SSR μ΄μμ΅λλ€. App Router μμλ dynamic = "force-dynamic" μ λͺ
μνλ©΄ λ§€ μμ²λ§λ€ μλ²μμ DB λ₯Ό μ‘°νν΄μ νμ΄μ§λ₯Ό λ λλ§ν©λλ€.
ts
// apps/blog/src/app/blog/[postKey]/page.tsx
export const dynamic = "force-dynamic";
λΈλ‘κ·Έμ΄κΈ° λλ¬Έμ κ²μκΈμ΄ μμ£Ό λ°λμ§ μμ§λ§, μΌλ¨ κ°μ₯ λ¨μν λ°©μμΌλ‘ μ΄μμ μμνμ΅λλ€. λ§€ μμ²λ§λ€ DB λ₯Ό μ‘°ννλ νμ μ΅μ λ°μ΄ν°λ₯Ό 보μ¬μ€ μ μμκ³ , 볡μ‘ν μΊμ μ€μ λ νμ μμμ΅λλ€.
SSR λ‘ μ΄μνλ μ€ "κ²μκΈμ μμ£Ό λ°λμ§ μλλ° λ§€ μμ²λ§λ€ DB λ₯Ό μ‘°νν΄μΌ ν κΉ?" λΌλ μκ°μ΄ λ€μμ΅λλ€. κ·Έλμ μΊμ±μ λμ νκΈ°λ‘ νμ΅λλ€.
λ¨Όμ ISR μ μ μ©νμ΅λλ€. 1μκ°λ§λ€ νμ΄μ§λ₯Ό μ¬μμ±νλλ‘ revalidate = 3600 μΌλ‘ μ€μ νμ΅λλ€.
ts
export const revalidate = 3600;
κ·Έλ¬λ 1μκ° μΊμλ λΈλ‘κ·Έ νΉμ±μ λ무 μμ£Ό μ¬μμ±λλ κ² κ°μκ³ , λ°°ν¬ν λλ§λ€ μΊμλ₯Ό μ¦μ 무ν¨ννκ³ μΆμμ΅λλ€.
λ°°ν¬ μ§ν μΊμλ₯Ό μ¦μ κ°±μ νκΈ° μν΄ μ¨λ맨λ ISR λ°©μμΌλ‘ λ³κ²½νμ΅λλ€. revalidate = false λ‘ μ€μ ν΄ μλ μ¬μμ±μ λκ³ , λ°°ν¬ ν API λ₯Ό μ§μ νΈμΆν΄μ μΊμλ₯Ό 무ν¨ννλ λ°©μμ
λλ€.
ts
// apps/blog/src/app/api/revalidate/route.ts
export async function POST(request: NextRequest) {
const apiKey = request.headers.get("x-api-key");
if (apiKey !== process.env.VALIDATED_API_KEY) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
revalidatePath("/", "layout");
return NextResponse.json({ revalidated: true });
}
GitHub Actions μμ λ°°ν¬κ° μλ£λ ν μ΄ API λ₯Ό νΈμΆνλλ‘ CI νμ΄νλΌμΈμλ μΆκ°νμ΅λλ€.
μΊμλ₯Ό μμ ν νμ©νκΈ° μν΄ generateStaticParams λ₯Ό μΆκ°ν΄μ λΉλ μμ μ λͺ¨λ κ²μκΈ νμ΄μ§λ₯Ό μ μ μΌλ‘ μμ±νλλ‘ νμ΅λλ€.
ts
export const revalidate = false;
export async function generateStaticParams() {
try {
const posts = await getPosts("", "DESC");
return posts
.filter((post) => isPostVisible(post) && post.postKey !== null)
.map((post) => ({ postKey: post.postKey as string }));
} catch {
return [];
}
}
μμ ν SSG λ‘ μ ννλ©΄μ μμμΉ λͺ»ν λ¬Έμ λ€μ΄ μ°λ¬μ λ°μνμ΅λλ€.
첫 λ²μ§Έ λ¬Έμ λ Docker λΉλ νκ²½μμ DB μ μ°κ²°ν μ μλ€λ κ²μ΄μμ΅λλ€.
λΈλ‘κ·Έ DB λ GCP λ΄λΆ λ€νΈμν¬μμλ§ μ κ·Ό κ°λ₯ν©λλ€. GitHub Actions μμ Docker μ΄λ―Έμ§λ₯Ό λΉλν λλ GCP λ€νΈμν¬ μΈλΆμμ μ€νλκΈ° λλ¬Έμ DB μ μ°κ²°μ΄ λμ§ μμμ΅λλ€. κ²°κ³Όμ μΌλ‘ generateStaticParams κ° λΉ λ°°μ΄μ λ°νν΄μ μ΄λ€ κ²μκΈ νμ΄μ§λ μ μ μΌλ‘ μμ±λμ§ μμκ³ , λͺ¨λ κ²μκΈ URL μμ 404 κ° λ°μνμ΅λλ€.
μ΄λ₯Ό ν΄κ²°νκΈ° μν΄ λΉλ μμ μλ§ GCP λ°©νλ²½μ μμλ‘ μ΄κ³ , DB μ μ μ 보λ₯Ό Docker λΉλ μΈμλ‘ μ λ¬νλ λ°©μμΌλ‘ μ°ννμ΅λλ€.
yaml
# .github/actions/auto_deploy/action.yml
- name: Build Docker image
run: |
DB_USER=$(gcloud secrets versions access latest --secret=db-user)
# ... λ€λ₯Έ μν¬λ¦Ώλ€λ λμΌνκ² κ°μ Έμ΄
docker build \
--build-arg DB_USER="$DB_USER" \
# ... λ€λ₯Έ λΉλ μΈμλ€
-t $IMAGE_NAME:latest -f apps/blog/Dockerfile .
dockerfile
# apps/blog/Dockerfile ARG DB_USER ARG DB_HOST # ... ENV DB_USER=$DB_USER ENV DB_HOST=$DB_HOST RUN turbo run build --filter=blog
μ΄ λ°©μμΌλ‘ λΉλ μμ μ DB μ μ°κ²°ν΄μ κ²μκΈ λͺ©λ‘μ κ°μ Έμ€κ³ , κ° κ²μκΈ νμ΄μ§λ₯Ό μ μ μΌλ‘ μμ±νλ λ°λ μ±κ³΅νμ΅λλ€. νμ§λ§ λ€λ₯Έ λ¬Έμ κ° κΈ°λ€λ¦¬κ³ μμμ΅λλ€.
Cloud Run μ νΈλν½μ΄ μμΌλ©΄ 컨ν μ΄λλ₯Ό 0 μΌλ‘ μ€μ΄λ scale-to-zero κΈ°λ₯μ΄ μμ΅λλ€. λΉμ© μ κ°κ³Ό μμ ν¨μ¨νλ₯Ό μν κΈ°λ₯μΈλ°, SSG λ°©μκ³Ό μΆ©λνλ λ¬Έμ κ° μμμ΅λλ€.
SSG λ‘ μμ±λ μ μ νμ΄μ§λ€μ λΉλ μμ μ μμ±λμ΄ μ»¨ν
μ΄λ μ΄λ―Έμ§ λ΄λΆμ .next/ λλ ν 리μ ν¬ν¨λ©λλ€. scale-to-zero μ΄ν μ 컨ν
μ΄λκ° μμλλ©΄ μ΄λ―Έμ§μ .next/ νμΌμ κ·Έλλ‘ μμ΅λλ€. κ·Έλ¬λ λΉλ μ΄νμ DB μ μλ‘ μΆκ°λ κ²μκΈμ generateStaticParams μ ν¬ν¨λμ§ μμκΈ° λλ¬Έμ μ μ νμΌμ΄ μμ΅λλ€.
μ κ²μκΈμ κ²½μ° notFound() λ₯Ό λ°ννκ² λμ΄ μμ΄μ 404 κ° λ°μνμ΅λλ€. μ¨λ맨λ ISR λ‘ revalidatePath λ₯Ό νΈμΆν΄μ μΊμλ₯Ό κ°±μ νλ λ°©μμ μλνμ§λ§, λ°°ν¬ μ§νμλ μλνμ΄λ μ΄ν scale-to-zero λ‘ μΈν΄ 컨ν
μ΄λκ° μ¬μμλλ©΄ ISR μΊμκ° μ΄κΈ°νλμ΄ λμΌν λ¬Έμ κ° λ°λ³΅λμ΅λλ€.
λ°°ν¬ μλ£
β revalidatePath("/", "layout") νΈμΆ
β μΊμ κ°±μ μ±κ³΅
β νΈλν½ μμ β scale-to-zero λ°λ
β μ 컨ν
μ΄λ μμ β μΊμ μ΄κΈ°ν
β μ κ²μκΈ μμ² β 404
ν΅μ¬ λ¬Έμ λ Cloud Run μ 컨ν μ΄λ κΈ°λ°μ΄λΌ μνλ₯Ό μ μ§νμ§ μλλ€ λ μ μ λλ€. Next.js μ ISR μΊμλ 컨ν μ΄λ λ΄λΆ λ©λͺ¨λ¦¬μ νμΌ μμ€ν μ μ μ₯λλλ°, scale-to-zero λ‘ μ»¨ν μ΄λκ° μ¬μμλλ©΄ μΊμκ° μ΄κΈ°νλ©λλ€. λ°λΌμ ISR μ μ΄μ μ΄ μ¬μ€μ μ¬λΌμ§λλ€.
μ¬λ¬ λ²μ μλ λμ κ²°λ‘ μ λ΄λ Έμ΅λλ€. Cloud Run νκ²½μμλ SSG λ ISR μ μ μ§νλ κ²μ΄ μ€νλ € 볡μ‘μ±μ λμΌ λΏμ΄λΌλ κ²μ λλ€.
κ²°κ΅ generateStaticParams μ μ¨λ맨λ revalidate API λ₯Ό λͺ¨λ μ κ±°νκ³ , λ€μ dynamic = "force-dynamic" μΌλ‘ λμμμ΅λλ€.
ts
// μ΅μ’
μν
export const dynamic = "force-dynamic";
μ΄λ κ² νλ©΄ λ§€ μμ²λ§λ€ DB λ₯Ό μ‘°ννμ§λ§, νμ μ΅μ λ°μ΄ν°λ₯Ό 보μ¬μ£Όκ³ , μ κ²μκΈλ μ¦μ λ°μλλ©°, λΉλ μμ μ DB μ°κ²°λ νμ μμ΅λλ€. CI νμ΄νλΌμΈλ λ¨μν΄μ‘μ΅λλ€.
λ λλ§ μ λ΅μ "μ΄λ€ λ°©μμ΄ λ μ’μκ°" κ° μλλΌ "μ΄λ€ μΈνλΌ νκ²½μμ μ΄μνλκ°" μ λ°λΌ λ¬λΌμ§λ€λ κ²μ μ΄λ² κ²½νμΌλ‘ λ°°μ μ΅λλ€.
SSG λ Vercel μ²λΌ Next.js μ μ΅μ νλ νλ«νΌμμλ λ§€μ° κ°λ ₯ν©λλ€. ISR μΊμκ° μꡬμ μΌλ‘ μ μ§λκ³ , μ¨λ맨λ revalidation λ μμ μ μΌλ‘ μλν©λλ€. νμ§λ§ Cloud Run μ²λΌ 컨ν μ΄λλ₯Ό scale-to-zero λ‘ κ΄λ¦¬νλ νκ²½μμλ μΊμκ° λ μκ°κΈ° λλ¬Έμ ISR μ μ΄μ μ΄ λ°κ°λ©λλ€.
λΈλ‘κ·Έμ κ²μκΈ μκ° κ·Έλ κ² λ§μ§ μκ³ νΈλν½λ ν¬μ§ μμ μν©μμ SSR μ μ±λ₯ λΉμ©μ νμ© κ°λ₯ν μμ€μ λλ€. μ€νλ € λ¨μν SSR μ΄ μ μ§λ³΄μ μΈ‘λ©΄μμ λ λμ μ νμ΄μμ΅λλ€. λλ‘λ κ°μ₯ λ¨μν λ°©λ²μ΄ κ°μ₯ μ’μ λ°©λ²μ΄λΌλ κ²μ λ€μ νλ² λκΌμ΅λλ€.