서버 컴포넌트는 서버에서 랜더링 되며, 클라이언트는 결과만 보여주게 된다.
모든 연산이 서버에서 이루어지기 때문에 서버에서 다양한 캐시 전략을 취할 수 있다.
하지만, 다양한 캐시 전략때문에 개발자의 의도와는 다르게 사용자에게 오래된 데이터를 보여주기도 한다.
어떤 문제점이 있을까?
게시글 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" } ]
실행 결과, 현재 게시글 데이터가 잘 표시되고 있다.
그러나 데이터가 업데이트 되면 문제가 생길 수 있다.
몇가지 시나리오가 있다.
첫 번째 시나리오
- 게시글 페이지 최초 이동
- 게시글 데이터 업데이트
- 게시글 페이지 새로고침
→ 최신 데이터가 보여짐
두 번째 시나리오
- 게시글 페이지 최초 이동
- 게시글 데이터 업데이트
- 메인 페이지로 이동
- 다시 게시글 페이지로 이동
→ 오래된 데이터가 보여짐
세 번째 시나리오
- 게시글 페이지 최초 이동
- 게시글 데이터 업데이트
- 메인 페이지로 이동
- 30초 대기
- 다시 게시글 페이지로 이동
→ 최신 데이터가 보여짐
첫 번째 시나리오는 예상대로 최신 데이터가 잘 보여지지만, 두 번째 시나리오의 결과가 잘 이해되지 않을것이다.
원인을 이해하기 위해서는 클라이언트 컴포넌트와의 비교, 그리고
Next.js의 캐시 전략을 이해할 필요가 있다.
( 세 번째 시나리오는 우선 제외하고 생각하자. )
클라이언트 컴포넌트와의 비교
서버 컴포넌트를 사용하기 이전에는 이러한 문제를 크게 경험하지 못했다.
최신 data로 업데이트 해야한다면, 클라이언트에서 effect를 이용하여 최신 data를 fetching하여 해결할 수 있었다.
하지만, 서버 컴포넌트의 경우에는 클라이언트에서 effect는 물론 다른 스크립트를 실행하지 못한다.
서버 컴포넌트에서 data fetching은 전적으로 서버에서 처리된다.
클라이언트는 랜더링된 컴포넌트 결과물만 표시하기 때문에 아무것도 할 수 없다.
그렇다면 최신 데이터가 보이지 않는 이유는 서버에서 새로운 data fetching을 하지 않았기 때문인가?
그건 또 아니다..
네트워크 탭을 열고 두 번째 시나리오를 다시 해보면
- 게시글 페이지 최초 이동
( 최초 페이지와 자바스크립트 청크 및 css를 받아오는 모습이다 )
- 게시글 데이터 업데이트
- 메인 페이지로 이동
( 메인 페이지의 서버 컴포넌트를 받아온 모습이다 )
- 다시 게시글 페이지로 이동
( 아무런 변화가 없다.. 아무것도 요청하지 않는다.. )
왜 새로운 요청이 없지?
아무런 요청이 안되는 것을 봐서, 클라이언트 단에서 무언가 캐싱이 된다고 추정할 수 있다.
원인은 바로 Router Cache 때문인데, 클라이언트 단에서 동작하는 캐시이다.
- Router Cache는 사용자 세션이 지속되는 동안 개별 경로 세그먼트로 분할되어 있다.
- 각 세그멘트에는 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가 초기화되며 최신 게시글이 잘 반영되는 것을 확인 할 수 있었다.