개요

Next.js는 기본적으로 성능과 비용을 줄이기 위해 cache를 통해 데이터를 저장한다. 즉 라우트는 Statically rendered 된다.
이게 무슨 뜻이냐면, 데이터 요청을 하면 Next.JS는 Re-Rendering 하는 대신, cache에 저장된 데이터를 활용한다. 임의적으로 Opt-out 하지않는 이상 cache를 활용한다는 뜻이다.

위의 그림처럼 라우트가 동적 또는 정적 또는 정적으로 렌더링 되는지를 Caching으로 처리한다. Cache 된 상태이면 그 데이터를 그래로 활용하고 그렇지 않으면 Cache를 적용한다.

Router Cache

클라이언트 측에서 저장되는 Route Cache이다. 레이아웃, 로딩 상태, 페이지와 같은 React Server Component payload를 저장한다.

공식문서에 따르면 Layout과 Loading 상태는 navigation 할 때 마다 Cache되지만 Page의 경우 기본적으로 캐시되지 않지만 브라우저를 뒤로가기, 앞으로 가기 할 경우 Cache 데이터를 이용한다고 한다.

Duration

캐시는 브라우저의 임시 메모리에 저장.
캐시는 Session과 Automatic Invalidation Period 두 가지 요소에 의해 유지된다.

  • Session: cache는 navigation 동안 지속 refresh할 경우 사라짐

  • Automatic Invalidation Period: layouts과 loading states는 특정 시간 뒤면 invalidates

    기본적으로 Prefetched 속성에 따라 얼마나 지속되는지 영향 받음

Invalidation

revalidatePath, revalidateTag 로 캐시 무효화 할 수 있다.

Practice

// app/route_cache/a/page.tsx
import { revalidatePath } from "next/cache";
import Link from "next/link";
export default function page() {
  revalidatePath("/route_cache/a");
  console.log("a page is rendered.");
  return (
    <>
      <h1>a page</h1>
      <Link href="/">Home</Link>
    </>
  );
}

// app/route_cache/b/page.tsx
import Link from "next/link";

export default function page() {
  console.log("b page is rendered.");
  return (
    <>
      <h1>b page</h1>
      <Link href="/">Home</Link>
    </>
  );
}

// app/page.tsx
import { revalidatePath } from "next/cache";
import Link from "next/link";

export default function page() {
  revalidatePath("/");
  return (
    <>
      <h1>Home Page</h1>
      <Link className="border border-gray-500" href="/route_cache/a">
        a Page
      </Link>
      <Link className="border border-gray-500" href="/route_cache/b">
        b Page
      </Link>
    </>
  );
}


a와 home에 revalidatePath를 통해 cache를 무효화 시켰더니
정적 라우트에서 동적으로 렌더링 되도록 바뀐 것을 확인 할 수 있다.

실제로 a와 b page에 방문한 경우
렌더링 될 때 메시지를 출력해본 결과
a페이지가 렌더링 된다.

참고로 npm run build 상태여야 revalidate 된다.

Full Route Cahche

앞의 Route Cahche와 달리 Server에서 작동하는 Cache이다.

Cache가 영향을 주는 곳으로 총 4곳이 있다고 한다.

1. 서버에서 React 렌더링

RSC Payload로 렌더링
Client Component Javascript 명령어를 사용하여 HTML 렌더링
-> 브라우저가 업데이트 될 경우 렌더링되는데
이를 캐시에 저장하여 리렌더링 할 필요가 없어진다.

2. Next.js Caching on the Sever (Full Route Cache)

  • /a 페이지의 Full Route Cache가 없을 경우 원본 데이터를 Fetch 한다. 이후 RSC payload, HTML로 렌더링 하여 Cache로 set.

  • build 타임 동안 정적으로 렌더링되어 이후는 Re-Rendering 할 필요 없음

3. React Hydration and Reconciliation on the Client

4. Next.js Caching on the Client (Router Cache)

5. Subsequent Navigation

Route Cache와 Full Route Cache의 차이점

  • Route Cache는 Static Route, Dynamic Route 둘다 적용된다. 클라이언트 측에서 RSC Payload를 In-memory 형태로 저장한다.
  • Full Route Cache는 RSC Payload와 HTML을 지속적으로 저장한다. Dynamic Route에서a만 Cache.

Requset Memoization

fetch API에 자동적으로 같은 url로 여러번 요청할 때 요청을 memoize하여 한 번만 요청하여 실행하도록 함. Next.js 기능이 아닌 React 기능, Fetch 요청의 GET 메소드에서만 Memoization 적용

같은 데이터 요청이 반복되도 성능 하락 없이 memoization을 통해 데이터를 효율적으로 fetch 가능하다.

Request Memoization 작동 방식


위의 그림과 같이 같은 url로 fetch 요청을 할 경우 처음 cache가 안된 경우는 miss 상태이다. 그러면 Data를 Cache하고 이후의 요청에서는 Request Memoization이 set 되어 있으므로 url을 요청하지 않고 저장된 데이터를 사용한다.

예시 코드

// src/components/ServerComponents.tsx
"use server";

import { revalidateTag } from "next/cache";

export async function getUsers() {
  const res = await fetch("https://66a9d583613eced4eba65dd8.mockapi.io/users", {
    method: "GET",
    next: {
      revalidate: 60, // 60초 이후에 자동으로 revalidate
      tags: ["users"], // revalidateTag를 사용하기 위해 tag 지정
    },
    cache: "force-cache", // 강제로 cache 진행
  });
  return await res.json();
}

export async function revalidateUsers() {
  revalidateTag("users");
}

// src/app/requset_memoization/page.tsx
"use client";
import { getUsers, revalidateUsers } from "@/components/ServerComponents";
import { useState } from "react";

export default function RequestMemo() {
  const [users, setUsers] = useState([]);

  const onChangeHandler = async () => {
    setUsers(await getUsers());
  };
  const Revalidate = async () => {
    await revalidateUsers();
  };
  console.log(users);
  return (
    <>
      <h1>User length: {users.length}</h1>
      <button
        className="inline-block border border-slate-500"
        onClick={onChangeHandler}
      >
        Get user length
      </button>
      <button
        className="inline-block border border-slate-500"
        onClick={Revalidate}
      >
        Revalidate
      </button>
    </>
  );
}


데이터를 처음 요청한 이후 Request Memoization으로 인해 서버에 직접 요청하지 않아 시간이 감소한 것을 확인 할 수 있다.


Revalidate하여 데이터를 재요청한 모습

Data Cache

내장된 데이터 캐시는 데이터 fetch 할 때 동안 지속.
Next.js는 nativefetch API를 확장하여 각각의 요청이 서버에 설정되도록 영구적인 caching 하도록 만들었다.

기본적으로 fetch를 사용하는 데이터 요청은 cache 되지 않으므로 cachenext.revalidate option을 사용하여야 한다.

Data Cache의 작동 방식

  • 처음 'force-cache'를 속성을 가진 fetch 요청이 가면, Data Cache를 확인한다.
  • 캐시된 응답이 발견되지 않으면 데이터 소스로 부터 요청을 받아 Data Cache에 저장되고 memoized
  • uncached data('no-store')의 경우 항상 data source로 부터 fetch 되어 memoized
  • 데이터가 cached 또는 uncached 되든 안되는 중복된 요청을 파하기 위해 memoized 된다.

Data Cache와 Request Memoization의 차이

Data Cache

Request와 Deployments에 걸쳐 지속적
사용자가 강제적으로 cache하고 임의적으로 revalidate 하도록 주기나 트리거를 설정할 수 있다.

Memoization

요청의 lifetime 동안 지속한다. (only lasts the lifetime of a request.)

같은 렌더 경로에서 중복 요청의 수를 줄이기 위함.
원래 데이터 소스로의 요청의 수를 줄임.

Revalidating

Time-based Revalidation

시간 간격을 두고 데이터 revalidate
next.revalidate 옵션 사용

On-Demand Revalidation

revalidatePath, revalidateTag를 이용

Codes

Github: https://github.com/foopky/NextJS-Caching-Practice

Reference

공식문서: https://nextjs.org/docs/app/building-your-application/caching

0개의 댓글