Next.js(ver.12)

Docs_Data Fethcing(Incremental Static Regeneration)

ecoEarth 2023. 3. 19. 04:09

Incremental Static Regeneration

Next.js는 사이트를 배포한 이후에도 정적인 페이지를 새로 만들거나, 이미 생성된 정적인 페이지의 데이터를 업데이트할 수 있도록 도와준다. ISR은 사이트전체를 갈아엎지않고도 정적인 페이지를 개별단위로 생성할 수 있도록 도와준다. 또한 수백,수천페이지로 확장하면서도 정적인 페이지를 이용하는것의 이점을 유지하도록 해준다.

Note: experimental-edge런타임은 현재 ISR과 호환되지않지만, cache-control 헤더를 수동으로 세팅함으로써 stale-while-revalidate를 활용할 수 있다. 

 

ISR을 활용하기위해서 revalidate porp을 getStaticProps로 추가해줘야한다.

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 10 seconds
    revalidate: 10, // In seconds
  }
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: 'blocking' } will server-render pages
  // on-demand if the path doesn't exist.
  return { paths, fallback: 'blocking' }
}

export default Blog

 

빌드타임때 pre-render된 페이지가 클라이언트에 의해 처음 요청되었을때는 미리 저장된 페이지를 보여준다.

  • 첫번째 요청이후로 + 10초가 지나기 이전에는 역시 미리 저장되었던 페이지를 재활용해서 보여준다.
  • 10초가 지나면 여전히 저장된 페이지를 재활용해서 보여준다. 근데 이때 정적인 페이지의 데이터를 background에서 업데이트한다. 
  • 일단 정적인 페이지가 사용하는 데이터의 업데이트가 성공적으로 끝나면, Next.js는 저장된 파일을 '무효화'시키고 업데이트된 데이터를 이용해 새롭게 만들어진 정적인 페이지를 보여준다. 만약 정적인 페이지가 사용하는 데이터의 업데이트가 실패하면 오류를 내뿜거나 하지않고 그냥 여전히 이전에 저장된 페이지를 보여준다.

 

ISR with Static file VS ISR with Dynamic file

ISR을 사용하는 페이지에서도 정적파일을 사용하느냐, 동적파일을 사용하느냐에 따라 첫번째요청에 대한 응답, 그리고 두번째이후요청에 대한 응답이 달라진다.

ISR with Static file

  • 정적파일을 사용하는 ISR의 경우 빌드시점에 미리 페이지를 생성하여 정적파일로 저장한다.
  • 이후에 요청이 발생하면, 해당 페이지에 대한 캐시된 정적 파일을 바로 제공한다.

ISR with Dynamic file

  • 동적파일을 사용하는 ISR의 경우 빌드시점에 페이지를 미리 생성하지 않는다.
  • 이후 첫번째 요청이 발생하면, 서버에서 동적 데이터를 포함하여 페이지를 렌더링한다.
  • 일단 첫번째요청에 의해 페이지를 렌딩하고나면, 그 페이지를 캐시에 저장한다음 이후부터는
    ISR with Static file과 동일하게 작동한다.
⚠️caution
provider(상위 데이터 제공자)가 기본적으로 캐싱을 사용하는 경우, 해당 캐싱을 비활성화해야 할 수도 있다. 만약 캐싱이 활성화된 상태에서는 새로운 데이터를 가져와서 ISR 캐시를 업데이트할 수 없기 때문이다. 요청하는 엔드포인트가 Cache-Control 헤더를 반환하는 CDN (콘텐츠 전송 네트워크)에서 캐싱이 발생할 수 있다.

 

Using On-Demand Revalidation

만약 reavalidate의 value를 60초로 설정하면 모든 클라이언트는 동일하게 생성된 버전을 reavalidate가 일어나기 이전 1분동안 보게된다. 이경우 캐시를 무효화하는 방법은 그냥 reavalidate가 또다시 일어나는 1분뒤에 재방문하는 것이다.

근데 Next.js의 v12.2.0부터는 On-Demand Incremental Static Regeneration을 지원하여 특정 페이지의 캐시를 수동으로 삭제할 수 있게 되었다.

 

사용하는 방법은 다음과 같다.

우선, Next.js에서만 알고있는 시크릿 토큰을 생성한다. 이 시크릿 토큰은 무단 액세스를 방지하는 용도로 사용된다.

그런 다음 다음 URL구조로 라우트에 액세스할 수 있다.

https://<your-site.com>/api/revalidate?secret=<token>

그다음, 이 시크릿 토큰을 앱의 환경 변수로 추가한다음 마지막으로 재검증 API 라우트를 만들면 된다.

// pages/api/revalidate.js

export default async function handler(req, res) {
  // Check for secret to confirm this is a valid request
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    // this should be the actual path not a rewritten path
    // e.g. for "/blog/[slug]" this should be "/blog/post-1"
    await res.revalidate('/path-to-revalidate')
    return res.json({ revalidated: true })
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res.status(500).send('Error revalidating')
  }
}

https://on-demand-isr.vercel.app/

 

Testing on-Demand ISR during development

When running locally with next dev, getStaticProps is invoked on every request. To verify your on-demand ISR configuration is correct, you will need to create a production build and start the production server:

$ next build
$ next start

Then, you can confirm that static pages have successfully revalidated.

 

Error handling and revalidation

만약 getStaticProps가 데이터를 업데이트하는 상황에서 에러가 발생했거나, 개발자가 수동적으로 에러를 발생시키면 에러가 발생한시점 이전 cash에 성공한 최종 페이지를 보여준다. 그리고 다음번 요청에 맞추어 getStaticProps가 다시 실행된다.

export async function getStaticProps() {
  // If this request throws an uncaught error, Next.js will
  // not invalidate the currently shown page and
  // retry getStaticProps on the next request.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  if (!res.ok) {
    // If there is a server error, you might want to
    // throw an error instead of returning so that the cache is not updated
    // until the next successful request.
    throw new Error(`Failed to fetch posts, received status ${res.status}`)
  }

  // If the request was successful, return the posts
  // and revalidate every 10 seconds.
  return {
    props: {
      posts,
    },
    revalidate: 10,
  }
}

 

Self-hosting ISR

기본적으로 next start명령어를 사용해 배포할 경우 생성된 파일들은 각 pod의 메모리에 저장된다. 각 pod마다 고유한 정적파일 복사본을 가지고있기때문에 일부 pod에서는 오래된 데이터가 보여질 수 있다.

따라서 모든 pod에서의 일관성을 보장하기위해 인메모리 캐싱을 비활성화할 수 있다. 이렇게 하면 Next서버가 파일 시스템에서 ISR에 의해 생성된 파일만 사용하도록 명령한다.

Kubernetes pod에서 공유 네트워크 마운트를 사용하여 동일한 파일시스템 캐시를 다른 컨테이너에서 재사용할 수 있다.

동일한 마운트를 공유하면 next/image캐시를 포함하는 .next폴더도 공유되고 재사용된다. 

 

인메모리 캐싱을 비활성화하는 방법은 next.config.js파일에서 isrMemoryCacheSize를 0으로 설정하는 것이다.

module.exports = {
  experimental: {
    // Defaults to 50MB
    isrMemoryCacheSize: 0,
  },
}

 

⚠️caution
You might need to consider a race condition between multiple pods trying to update the cache at the same time, depending on how your shared mount is configured.