14. Next.js Router Cache 이해하기
14. Next.js Router Cache 이해하기

14. Next.js Router Cache 이해하기

Authors
Date
Tags
Published
Published
Slug
 
 
서버 컴포넌트는 서버에서 랜더링 되며, 클라이언트는 결과만 보여주게 된다.
모든 연산이 서버에서 이루어지기 때문에 서버에서 다양한 캐시 전략을 취할 수 있다.
하지만, 다양한 캐시 전략때문에 개발자의 의도와는 다르게 사용자에게 오래된 데이터를 보여주기도 한다.

 
 
 
 

어떤 문제점이 있을까?

게시글 data를 fetch하여 보여주는 간단한 애플리케이션을 구현해보았다.
 
<!--layout.tsx 일부--> <nav className="flex px-96 p-10 justify-around font-bold text-xl border-b-2 border-gray-700"> <Link href="/" className="hover:text-blue-500">메인</Link> <Link href="/posts" className="hover:text-blue-500">게시글</Link> </nav>
layout.tsx에는 위와 같이 네비게이션 바가 구현되어있고
 
// posts/page.tsx type Post = { id: number; title: string; description: string; }; const getPosts = async () => { const res = await fetch("https://{URL}/api/v1/posts", { cache: "no-store" }); if (!res.ok) { throw new Error("Failed to fetch posts"); } return res.json() as Promise<Post[]>; }; export default async function Posts() { const posts = await getPosts(); return ( <div className="p-4"> {posts.map((post) => ( <ul key={post.id} className="p-10 border-4 border-y-2 border-blue-500"> <p>ID: {post.id}</p> <p>TITLE: {post.title}</p> <p>DESCRIPTION: {post.description}</p> </ul> ))} </div> ) }
page.tsx에는 위와같이 게시글 data를 fetch하고 리스트 형태로 사용자에게 보여진다.
 
 
[ { "title": "salmon", "description": "payment", "id": "1" }, { "title": "bus", "description": "finally", "id": "2" } ]
notion image
 
실행 결과, 현재 게시글 데이터가 잘 표시되고 있다.
 
 
 
 
 
그러나 데이터가 업데이트 되면 문제가 생길 수 있다.
몇가지 시나리오가 있다.
 

첫 번째 시나리오

  1. 게시글 페이지 최초 이동
  1. 게시글 데이터 업데이트
  1. 게시글 페이지 새로고침
→ 최신 데이터가 보여짐
 

두 번째 시나리오

  1. 게시글 페이지 최초 이동
  1. 게시글 데이터 업데이트
  1. 메인 페이지로 이동
  1. 다시 게시글 페이지로 이동
→ 오래된 데이터가 보여짐
 

세 번째 시나리오

  1. 게시글 페이지 최초 이동
  1. 게시글 데이터 업데이트
  1. 메인 페이지로 이동
  1. 30초 대기
  1. 다시 게시글 페이지로 이동
→ 최신 데이터가 보여짐
 
 
 
첫 번째 시나리오는 예상대로 최신 데이터가 잘 보여지지만, 두 번째 시나리오의 결과가 잘 이해되지 않을것이다.
원인을 이해하기 위해서는 클라이언트 컴포넌트와의 비교, 그리고 Next.js의 캐시 전략을 이해할 필요가 있다.
( 세 번째 시나리오는 우선 제외하고 생각하자. )
 

 
 
 
 

클라이언트 컴포넌트와의 비교

서버 컴포넌트를 사용하기 이전에는 이러한 문제를 크게 경험하지 못했다.
최신 data로 업데이트 해야한다면, 클라이언트에서 effect를 이용하여 최신 data를 fetching하여 해결할 수 있었다.
 
하지만, 서버 컴포넌트의 경우에는 클라이언트에서 effect는 물론 다른 스크립트를 실행하지 못한다.
서버 컴포넌트에서 data fetching은 전적으로 서버에서 처리된다.
클라이언트는 랜더링된 컴포넌트 결과물만 표시하기 때문에 아무것도 할 수 없다.
 
그렇다면 최신 데이터가 보이지 않는 이유는 서버에서 새로운 data fetching을 하지 않았기 때문인가?
그건 또 아니다..
 
 
네트워크 탭을 열고 두 번째 시나리오를 다시 해보면
 
  1. 게시글 페이지 최초 이동
    1. notion image
      ( 최초 페이지와 자바스크립트 청크 및 css를 받아오는 모습이다 )
  1. 게시글 데이터 업데이트
  1. 메인 페이지로 이동
    1. notion image
      ( 메인 페이지의 서버 컴포넌트를 받아온 모습이다 )
  1. 다시 게시글 페이지로 이동
    1. notion image
      ( 아무런 변화가 없다.. 아무것도 요청하지 않는다.. )
 

 
 
 
 
 

왜 새로운 요청이 없지?

아무런 요청이 안되는 것을 봐서, 클라이언트 단에서 무언가 캐싱이 된다고 추정할 수 있다.
원인은 바로 Router Cache 때문인데, 클라이언트 단에서 동작하는 캐시이다.
 
  • Router Cache는 사용자 세션이 지속되는 동안 개별 경로 세그먼트로 분할되어 있다.
    • notion image
  • 각 세그멘트에는 RSC payload를 저장하는 캐시가 있다.
  • 방문한 경로가 캐시되므로 페이지를 다시 로드하지 않고 즉각적으로 이동할 수 있다.
  • prefetch 옵션이 true라면 5분, 아니라면 30초동안 캐시가 지속된다.
 

 
 
 
 
 

해결방법

그렇다면 컴포넌트를 새롭게 업데이트 하기 위해선 Router Cache를 무효화 해야한다.
캐시를 무효화 하기 위해선 두가지 방법이 있다
  • 30초를 기다린다.
  • router.refresh() 또는 revalidatePath 또는 revalidateTag 메소드를 사용한다.
 
30초를 기다리는 방법은 사용자의 액션이므로 해결방법이 되지 못한다. (위에서 스킵했던 세 번째 시나리오이다)
그렇다면 router.refresh() 또는 revalidatePath 또는 revalidateTag 메소드를 사용하는 것이다.
 
페이지를 이동할때마다 router.refresh() 또는 revalidatePath 또는 revalidateTag 메소드를 호출해야하는데, 페이지 이동을 감지하려면 클라이언트 컴포넌트가 필요하다.
"use client" import {useRouter} from "next/navigation"; import {useEffect} from "react"; export default function Refresh() { const router = useRouter(); useEffect(() => { router.refresh(); }, [router]); return null; }
위와같은 Refresh 컴포넌트를 정의하고 page.tsx에 추가하면, 페이지 이동마다 Router Cache가 초기화 되므로 새로운 데이터를 확인할 수 있다.

 
 
 
 
 

더 좋은 방법은 없나..

서버 컴포넌트에 억지로 클라이언트 컴포넌트를 끼워넣어 해결하는것이 좋은 방법처럼 보이지는 않는다.
더 좋은 방법이 없을까?
 
관련한 토론글이 존재했고, 최근에 staletime이라는 개념을 도입하는 것으로 보인다.
 
staletime은 현재 experimental 기능으로 v14.2.0-canary.53부터 사용이 가능하다.
 
사용법은 next.config.js에 아래와 같은 옵션을 추가하면 된다.
/** @type {import('next').NextConfig} */ const nextConfig = { experimental: { staleTimes: { dynamic: 1, static: 180, }, }, } module.exports = nextConfig
dynamic에는 prefetch 옵션이 true 또는 null일 경우 캐시 무효화까지 걸리는 시간,
static에는 prefetch 옵션이 false일 경우 캐시 무효화까지 걸리는 시간을 정해주면 된다.
 
이제 1초마다 /posts 페이지의 Router Cache가 초기화되며 최신 게시글이 잘 반영되는 것을 확인 할 수 있었다.