NextJS 의 사전 λ Œλ”λ§ 방식 - (1) Static Site Generation (using getStaticProps, getStaticPaths)

1. getStaticProps μ •μ˜

NextJS-getStaticProps

μ •μ˜ : λΉŒλ“œν•˜λŠ” λ™μ•ˆ νŽ˜μ΄μ§€λ₯Ό 사전 μƒμ„±ν•˜λŠ” 것

(pre-generate a page with data prepared on the server-side during build time.)

사전 μƒμ„±μ΄λž€ λͺ¨λ“  νŽ˜μ΄μ§€μ˜ html ꡬ쑰와 λͺ¨λ“  데이터λ₯Ό 사전에 μ€€λΉ„μ‹œμΌœ λ†“λŠ”λ‹€λŠ” 뜻이며 보톡 μ„œλ²„ μ‚¬μ΄λ“œμ—μ„œλ§Œ μ‹€ν–‰λ˜λŠ” μ½”λ“œλ₯Ό λΉŒλ“œ ν”„λ‘œμ„ΈμŠ€λ™μ•ˆ μ‹€ν–‰λ˜λ„λ‘ ν—ˆμš©ν•˜λŠ” 것을 μ˜λ―Έν•œλ‹€.

NextJS 에 μ–΄λ–€ νŽ˜μ΄μ§€λ₯Ό 사전에 생성해야 ν•˜λŠ”μ§€, 사전 μƒμ„ ν•œ νŽ˜μ΄μ§€μ— μ–΄λ–€ 데이터가 ν¬ν•¨λ˜μ–΄μ•Ό ν•˜λŠ”μ§€ μ§€μ •ν•˜κΈ° μœ„ν•΄ getStaticProps λΌλŠ” μ΄λ¦„μ˜ 비동기 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•œλ‹€.

pages/ 경둜 내에 μžˆλŠ” μ»΄ν¬λ„ŒνŠΈ λ‚΄λΆ€μ—μ„œλ§Œ μ‚¬μš© κ°€λŠ₯ν•˜λ©°, jsx λ₯Ό λ¦¬ν„΄ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈ λ‚΄λΆ€μ—μ„œκ°€ μ•„λ‹Œ 별도 ν•¨μˆ˜λ₯Ό μ„ μ–Έ 및 export ν•œλ‹€.

getStaticProps λ₯Ό μ„ μ–Έν•˜λ©΄ nextjs 둜 ν•˜μ—¬κΈˆ 이 νŽ˜μ΄μ§€λŠ” 사전 생성이 λ˜μ–΄μ•Ό ν•œλ‹€λŠ” 것을 μ•Œλ €μ€€λ‹€.

2. getStaticProps의 κΈ°λ³Έ μ‚¬μš© μ˜ˆμ‹œ

  • props : 기본적으둜 ν•΄λ‹Ή μ»΄ν¬λ„ŒνŠΈμ—μ„œ 받을 props λ₯Ό 리턴해야 ν•˜λ©° μœ„μ—μ„œλŠ” 더미 데이터λ₯Ό fs 둜 λΆˆλŸ¬μ™€ λ„£μ–΄μ£Όκ³  μžˆλ‹€.

  • revalidate : μ–΄λ–€ νŽ˜μ΄μ§€μ˜ 데이터가 자주 λ°”λ€ŒλŠ” 경우 맀번 λΉŒλ“œλ₯Ό ν•΄μ£ΌκΈ°μ—λŠ” λ¦¬μ†ŒμŠ€κ°€ λ‚­λΉ„λœλ‹€. 첫 배포 이후 재배포 없이 νŽ˜μ΄μ§€λ₯Ό μž¬μƒμ„±ν•˜λ„λ‘ ν•˜λŠ”λ°, revalidate: 10 은 ν•΄λ‹Ή νŽ˜μ΄μ§€κ°€ 10초 λ§ˆλ‹€ μž¬μƒμ„± λ˜μ–΄μ•Ό ν•œλ‹€λŠ” 것을 μ•Œλ €μ€€λ‹€.

    export async function getStaticProps() {
    // μ—¬κΈ° μ•ˆμ—μ„œ fs λͺ¨λ“ˆμ„ μ“Έ 수 μžˆλ‹€.
    
    console.log('(Re-)Generating...')
    const filePath = path.join(process.cwd(), 'data', 'dummy-backend.json')
    const jsonData = await fs.readFile(filePath)
    
    const data = JSON.parse(jsonData)
    
    if (!data) {
      return {
        redirect: {
          destination: '/no-data',
        },
      }
    }
    
    if (data.products.length === 0) {
      return {
        notFound: true,
      }
    }
    
    return {
      props: {
        products: data.products,
      },
      revalidate: 10,
    }
    }

  • npm run build 이후, npm start (λΉŒλ“œ ν”„λ‘œλ•μ…˜ ready μ›Ή μ‚¬μ΄νŠΈλ₯Ό μ‹€ν–‰ν•˜λŠ” 것을 의미) 둜 μƒˆλ‘œκ³ μΉ¨μ„ 해보면 μœ νš¨μ„± μž¬κ²€μ‚¬κ°€ 이루어지고 μ΄λŠ” 10초 λ§ˆλ‹€ μž¬κ΅¬μΆ• λ˜λŠ” 것을 확인할 수 μžˆλ‹€. (ISR : Incremental Static Generation, 증뢄 정적 생성)

    build image

  • notFound : 데이터 페칭의 성곡 유무, 잘λͺ»λœ 경둜 μ ‘κ·Ό 등에 따라 μ„€μ •ν•΄ 쀄 수 μžˆλŠ” μ˜΅μ…˜, true 이면 일반 νŽ˜μ΄μ§€ λŒ€μ‹ μ— 404 였λ₯˜ νŽ˜μ΄μ§€λ₯Ό λ Œλ”λ§ν•œλ‹€.

  • redirect : νŽ˜μ΄μ§€ μ½˜ν…μΈ λ‚˜ μ»΄ν¬λ„ŒνŠΈ μ½˜ν…μΈ λ₯Ό λ Œλ”λ§ν•˜μ§€ μ•Šκ³  λ‹€λ₯Έ νŽ˜μ΄μ§€, 즉 λ‹€λ₯Έ 라우트둜 λ¦¬λ””λ ‰μ…˜ ν•˜λŠ” 것

3. 동적 μ»΄ν¬λ„ŒνŠΈ, νŽ˜μ΄μ§€ 이동 ν›„ 데이터 페칭

Root 인덱슀 νŽ˜μ΄μ§€μ• μ„œ getStaticProps λ₯Ό 톡해 λ‹€μŒκ³Ό 같은 더미 데이터λ₯Ό props 둜 뢈러올 수 μžˆμ—ˆλ‹€.

{
  "products": [
    { "id": "p1", "title": "Product 1", "description": "This is product 1" },
    { "id": "p2", "title": "Product 2", "description": "This is product 2" },
    { "id": "p3", "title": "Product 3", "description": "This is product 3" }
  ]
}

products 리슀트 듀을 ul κ³Ό li λ₯Ό 톡해 λ Œλ”λ§ ν•΄μ£Όκ³  <Link /> νƒœκ·Έλ₯Ό 톡해 /productId 경둜둜 λ“€μ–΄κ°€μ„œ μ„ΈλΆ€ μƒν’ˆ 정보λ₯Ό ν™•μΈν•˜λ„λ‘ ν•˜μ˜€λ‹€.

그러면 ν•΄λ‹Ή νŽ˜μ΄μ§€μ˜ μ»΄ν¬λ„ŒνŠΈ μ—μ„œλ„ νŽ˜μ΄μ§€ 사전 μƒμ„±ν•¨μˆ˜ getStaticProps λ₯Ό ν•„μš”λ‘œ ν•  것이닀.

getStaticProps 의 context 인자λ₯Ό 톡해 동적 λΌμš°νŒ…μ˜ 경둜 이름을 뢈러 올 수 μžˆλ‹€.

export async function getStaticProps(context) {
  const { params } = context

  const productId = params.pid

  const data = await getData()

  const filteredData = data.products.find(product => product.id === productId)

  if (!filteredData) {
    return {
      notFound: true,
    }
  }
  return {
    props: {
      productDetail: filteredData,
    },
    revalidate: 10,
  }
}

근데 μ•„λž˜μ™€ 같은 μ—λŸ¬κ°€ λœ¬λ‹€.

4. 동적 μ»΄ν¬λ„ŒνŠΈ, νŽ˜μ΄μ§€λ₯Ό μœ„ν•œ getStaticPaths 의 κ°œμš”.

pages λ‚΄λΆ€ μ»΄ν¬λ„ŒνŠΈ 쀑, [pid].js, [pid].tsx λ“±μ˜ 동적 νŽ˜μ΄μ§€λ₯Ό μœ„ν•œ μ»΄ν¬λ„ŒνŠΈλŠ” NextJS μ—μ„œ κΈ°λ³Έ λ™μž‘μœΌλ‘œ νŽ˜μ΄μ§€λ₯Ό 사전 μƒμ„±ν•˜μ§€ μ•ŠλŠ”λ‹€.
NextJS μ—μ„œλŠ” 사전에 이 동적 νŽ˜μ΄μ§€λ₯Ό μœ„ν•΄ μ–Όλ§ˆλ‚˜ λ§Žμ€ νŽ˜μ΄μ§€λ₯Ό 미리 생성해야 ν•˜λŠ”μ§€ μ•Œμ§€ λͺ»ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

getStaticPaths 비동기 ν•¨μˆ˜λŠ” 동적 νŽ˜μ΄μ§€μ—μ„œ μ–΄λ–€ μΈμŠ€ν„΄μŠ€κ°€ 사전 μƒμ„±λ˜μ–΄μ•Ό ν•  μ§€ NextJS 에 μ•Œλ €μ€„ 수 μžˆλ‹€.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { pid: 'p1' } },
      { params: { pid: 'p2' } },
      { params: { pid: 'p3' } },
    ],
    fallback: false,
  }
}

path λ°°μ—΄ λ‚΄ 생성할 νŽ˜μ΄μ§€μ˜ μΈμŠ€ν„΄μŠ€λ₯Ό μœ„μ™€ 같은 ν˜•νƒœλ‘œ λ„£μ–΄μ€€λ‹€.

ν•˜μ§€λ§Œ, μƒν’ˆμ΄ 많이 μžˆλ‹€λ©΄ λͺ¨λ“  μƒν’ˆμ„ λ‹€ 사전 μƒμ„±ν•˜κΈ°μ—” μ‹œκ°„κ³Ό μžμ› 낭비이며 μΌλΆ€μ˜ μƒν’ˆ 상세 νŽ˜μ΄μ§€λŠ” 방문객이 거의 μ—†λ‹€κ³  ν•˜λ©΄ 이 λ˜ν•œ λ¦¬μ†ŒμŠ€ 낭비이닀.

5. getStaticPaths μ—μ„œμ˜ fallback 의 μ—­ν• 

fallback 을 true 둜 μ„€μ • μ‹œ, paths 에 ν¬ν•¨λ˜μ§€ μ•Šμ€ μΈμŠ€ν„΄μŠ€λΌλ„ νŽ˜μ΄μ§€ λ°©λ¬Έ μ‹œ λ‘œλ”©λ˜λŠ” 값이 μœ νš¨ν•  수 μžˆλ„λ‘ ν•΄μ€€λ‹€.

λ‹€λ§Œ μ΄λŠ” 사전 μƒμ„±λ˜λŠ” 것이 μ•„λ‹Œ μš”μ²­μ΄ μ„œλ²„μ— λ„λ‹¬ν•˜λŠ” μˆœκ°„+μ‹œμ  에 μƒμ„±λœλ‹€.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { pid: 'p1' } },
      //   { params: { pid: 'p2' } },
      //   { params: { pid: 'p3' } },
    ],
    fallback: false,
  }
}

κ·Έλ ‡κΈ° λ•Œλ¬Έμ— fallback: true 둜 μ„€μ • μ‹œ, url μž…λ ₯으둜 μ ‘κ·Όν•˜λ©΄ μ—λŸ¬κ°€ λ‚œλ‹€.

localhost:3000/p3, props 의 detail 정보가 μ—†λ‹€λŠ” Error λ°œμƒν•¨

이 κ²½μš°λŠ” κ·Έλž˜μ„œ 두가지 방법이 μžˆλŠ”λ°, ν•˜λ‚˜λŠ” ν΄λΌμ΄μ–ΈνŠΈ λ‹¨μ—μ„œ props 둜 λ°›λŠ” λ°μ΄ν„°μ˜ μœ λ¬΄μ— 따라 loading 처리 λ“±μœΌλ‘œ λΆ„κΈ°μ‹œμΌœμ„œ 화면을 λ Œλ”λ§ν•΄μ£ΌλŠ” 방법, λ‘λ²ˆμ§ΈλŠ” fallback: β€œblocking” 으둜 μ„€μ •ν•˜λŠ” 방법 이 μžˆλ‹€.

  • ν™”λ©΄ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ„κΈ°ν•˜κΈ°

    function ProductDetailPage(props) {
    const { productDetail } = props
    
    if (!productDetail) {
      return <p>Loading...</p>
    }
    
    return (
      <>
        <h1>{productDetail.title}</h1>
        <p>{productDetail.description}</p>
      </>
    )
    }
    
    export default ProductDetailPage
  • fallback: β€œblocking” 으둜 μ„€μ •

    fallback: β€œblocking” 으둜 μ„€μ • μ‹œ μœ„μ™€ 같이 ν΄λΌμ΄μ–ΈνŠΈ μ»΄ν¬λ„ŒνŠΈμ—μ„œ λΆ„κΈ°ν•  ν•„μš”κ°€ μ—†λ‹€. NextJS μ—μ„œ λ°›μ•„μ˜€λŠ” 데이터λ₯Ό κΈ°λ‹€λ €μ€€λ‹€. 단점은 νŽ˜μ΄μ§€ λ°©λ¬Έμžκ°€ μ‘λ‹΅λ°›λŠ” μ‹œκ°„μ΄ κΈΈμ–΄μ§ˆ 수 μžˆλ‹€λŠ” 점이닀.

6. 전체 [pid].js μ½”λ“œ

import React from 'react'
import fs from 'fs/promises'
import path from 'path'

function ProductDetailPage(props) {
  const { productDetail } = props

  if (!productDetail) {
    return <p>Loading...</p>
  }

  return (
    <>
      <h1>{productDetail.title}</h1>
      <p>{productDetail.description}</p>
    </>
  )
}

export default ProductDetailPage

async function getData() {
  const filePath = path.join(process.cwd(), 'data', 'dummy-backend.json')
  const jsonData = await fs.readFile(filePath)

  const data = JSON.parse(jsonData)

  return data
}

export async function getStaticProps(context) {
  const { params } = context

  const productId = params.pid

  const data = await getData()

  const filteredData = data.products.find(product => product.id === productId)

  if (!filteredData) {
    return {
      notFound: true,
    }
  }
  return {
    props: {
      productDetail: filteredData,
    },
    revalidate: 10,
  }
}

export async function getStaticPaths() {
  const data = await getData()

  const ids = data.products.map(product => product.id)

  const pathsWithParams = ids.map(id => ({
    params: {
      pid: id,
    },
  }))
  return {
    // paths: [
    //   { params: { pid: "p1" } },
    //   //   { params: { pid: "p2" } },
    //   //   { params: { pid: "p3" } },
    // ],
    paths: pathsWithParams,
    // fallback: false,
    fallback: true,
    // fallback: "blocking",
  }
}

7. conclusion

사전 λ Œλ”λ§μ€ 졜초 λ‘œλ”©μ‹œμ—λ§Œ 영ν–₯을 λ―ΈμΉœλ‹€. 이후 λΆ€ν„°λŠ” SPA λ°©μ‹μœΌλ‘œ React κ°€ ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ λͺ¨λ“  처리λ₯Ό μˆ˜ν–‰ν•œλ‹€.


[DotoriMook]
Written by@[DotoriMook]
ν”„λ‘ νŠΈμ—”λ“œ μ£Όλ‹ˆμ–΄ 개발자 λ„ν† λ¦¬λ¬΅μ˜ 기술개발 λΈ”λ‘œκ·Έ μž…λ‹ˆλ‹€.

GitHubMediumTwitterFacebook