[Next.js v14] 캐싱 이해 및 구성하기

·2024년 6월 27일
0

NextJS

목록 보기
24/26
post-thumbnail

📌 캐싱(Caching)

📖 NextJS의 캐싱 유형 이해하기

🔗 Next.js 공식문서 : Caching in Next.js

NextJS는 공격적인 캐싱을 수행한다. 네 가지의 영역에서 데이터나 페이지를 캐싱한다.

  1. 요청 기억(Request Memorization) : NextJS가 동일한 설정을 가진 데이터 요청을 저장하여 중복 요청을 방지한다. → NextJS 서버에서 처리하는 단일 요청에서 발생한다.
  1. 데이터 캐시(Data Cache) : 데이터 소스에서 변경되지 않은 경우 데이터를 저장하고 재사용하는 것이다. 데이터가 변경되지 않는 한 요청 자체를 피한다. → 사용자가 수동으로 재검증할 때까지 지속된다. 혹은 사용자가 설정한 특정 시간이 지날때까지 지속
  1. 전체 라우트 캐시(Full Route Cache) : NextJS가 전체 페이지, 전체 HTML 코드 및 전채 리액트 서버 컴포넌트 페이로드를 내부적으로 관리하고 이를 사용해 해당 페이지들을 렌더링한다. 전체 HTML 페이지가 다시 렌더링하는 것을 완전히 피한다. 기존 페이지를 재사용할 수 있기 때문에 페이지를 더 빠르게 만든다. → 이 캐시는 관련 데이터 캐시가 재검증될 때까지 지속되며 업데이트된 데이터가 있을때만 페이지가 다시 렌더링된다.
  1. 라우터 캐시(Router Cache) : NextJS는 브라우저의 메모리에 일부 리액트 서버 컴포넌트 페이로드를 저장하여 페이지 간의 이동이 더 빨리 일어날 수 있도록 한다. → 이 캐시는 서버에 의해 새로운 페이지가 렌더링될 때 혹은 NextJS로 렌더링 된 웹사이트를 벗어났다가 다시 돌아올 때 무효화된다.

요청 기억 캐시, 데이터 캐시, 전체 라우트 캐시는 서버 측에서 관리된다. 그러나 라우터 캐시는 클라이언트 측에서 관리되는 캐시이다.


📖 요청 메모화 처리 - Request Memorization

# backend 터미널 로그
2024-06-27T03:34:32.704Z: EXECUTING /messages on backend from page
2024-06-27T03:34:32.711Z: EXECUTING /messages on backend from layout

이 로그들은 각각 /app/messages/page.js와 /app/messages/layout.js를 통해 발생한다.

// page.js
import Messages from '@/components/messages';

export default async function MessagesPage() {
  const response = await fetch('http://localhost:8080/messages', {
    headers: {
      'X-ID': 'page',
    },
  });
  const messages = await response.json();

  if (!messages || messages.length === 0) {
    return <p>No messages found</p>;
  }

  return <Messages messages={messages} />;
}


// layout.js
export default async function MessagesLayout({ children }) {
  const response = await fetch('http://localhost:8080/messages', {
    headers: {
      'X-ID': 'layout',
    },
  });
  const messages = await response.json();
  const totalMessages = messages.length;

  return (
    <>
      <h1>Important Messages</h1>
      <p>{totalMessages} messages found</p>
      <hr />
      {children}
    </>
  );
}

각 fetch 요청은 headers만 다를 뿐이다. 따라서 헤더를 제외하고 요청을 보내보았다. 그렇게 되면 두 파일 모두 "http://localhost:8080/messages"로 요청 서명을 가지게 된다.

다시 백엔드 로그를 확인해보면 아래의 로그가 보이게 된다. (NextJS를 초기화하고 다시 /messages로 접근하면 아래의 로그만 보인다.)

2024-06-27T03:40:41.869Z: EXECUTING /messages on backend from undefined
  • 두 파일에서 다른 헤더를 보내지 않기 떄문에 어디에서 요청을 보내는지 알 수 없지만 하나의 요청만 보임을 알 수 있다. → 요청 기억(Request Memorization)
  • 백엔드로 전송된 fetch 요청의 경우 NextJS는 불필요한 요청을 피하고 대신 하나의 요청만 보내고 필요한 응답을 어플리케이션의 모든 부분에서 재사용한다.

📖 데이터 캐시 및 캐시 설정에 대한 이해

위의 상태에서 페이지를 다시 로드하거나 다른 페이지로 갔다가 돌아와도 백엔드로 더이상 요청이 전송되지 않는다. (새로운 로그가 추가되지 않음) → 데이터 캐시(Data Cache) 때문

NextJS는 백엔드에서 데이터를 가져오는 fetch 함수를 사용할 때 응답 데이터를 내부적으로 관리되는 서버 측 캐시에 저장하고 계속해서 그 데이터를 사용한다.

💎 재검증 방법 1. revalidatePath

재검증을 하게 되면 해당 캐시를 다시 업데이트할 수 있게 되는데, 이때 revalidatePath를 사용할 수 있다. → NextJS에게 특정 경로(특정 페이지의 데이터)가 변경되었다는 것을 알리고 해당 데이터 캐시를 삭제하도록 알릴 수 있다.

💎 재검증 방법 2. fetch 시 헤더에 cache 설정 추가하기

fetch을 할 때 헤더를 통해 캐시 설정을 할 수 있다. 이 설정은 브라우저 측에서 fetch를 사용할 때와 서버 측에서 fetch를 사용할 때 수 있는 설정이다.
중요한 것은 서버 측에서 NextJS가 기본적으로 제공하는 내장 함수를 오버라이딩 한다는 것이다. 그 이유는 설정한 캐시 설정을 찾아 NextJS 캐시의 동작을 변경하기 위해서이다.

// page.js
const response = await fetch("http://localhost:8080/messages", {
  cache: "force-cache",
});
  • 'force-cache' : 기본값, 데이터가 가능한 한 캐시되어 재사용되도록 지시
  • 'no-store' : NextJS에게 어느 한 곳에서 발생하는 fetch 요청(동일한 요청이 보내지는 다른 위치가 아닌)의 데이터가 캐시되지 않아야 한다로 알려준다. → 새 요청을 항상 전송하고 그 요청의 응답 데이터를 사용하도록 강제하며 데이터를 캐시하고 재사용하지 않는다.

cache: 'no-store'로 설정하게 되면 해당 페이지에 접속할 때마다 백엔드의 로그 메시지가 추가된다.

2024-06-27T03:56:20.183Z: EXECUTING /messages on backend from undefined
2024-06-27T03:56:20.184Z: EXECUTING /messages on backend from undefined
2024-06-27T03:56:20.667Z: EXECUTING /messages on backend from undefined
2024-06-27T03:56:20.668Z: EXECUTING /messages on backend from undefined

💎 재검증 방법 3. fetch 시 헤더에 next 설정 추가하기

데이터를 재검증하는 또다른 방법은 요청을 재구성하는 것인데 이번엔 캐시 설정이 아니라 NextJS에서 fetch API를 사용할 때 구성할 수 있는 특별한 next 설정을 사용한다.
NextJS는 fetch를 확장하여 새로운 설정들을 추가했다. 이 next 설정에는 revalidate와 같은 설정이 있다.

// page.js
const response = await fetch("http://localhost:8080/messages", {
  next: {
    revalidate: 5, // NextJS가 캐시 데이터를 재사용해야할 초 숫자 => 5초 뒤 새로운 요청
  },
});

📖 데이터 캐싱 제어

fetch의 헤더를 통해 캐시 제거를 설정하였다. 해당 헤더를 추가하지 않고도 파일 전체에 대한 설정을 추가할 수도 있는데 해당 파일에 여러 컴포넌트들이 있고 모든 컴포넌트에서 fetch 요청을 보낼 때 사용한다.

이 방법을 사용하면 모든 요청을 단계별로 수동으로 구성하는 대신 전체 파일에 대한 설정을 구성할 수 있으며 이 설정은 파일 내 어디서나 보내지는 모든 요청에 사용된다.

// page.js
import Messages from "@/components/messages";
import { unstable_noStore } from "next/cache";

// export const revalidate = 5; // 방법 1
// export const dynamic = 'force-dynamic'; // 방법 2

export default async function MessagesPage() {
  //   unstable_noStore(); // 방법 3
  const response = await fetch("http://localhost:8080/messages");
  const messages = await response.json();

  if (!messages || messages.length === 0) {
    return <p>No messages found</p>;
  }

  return <Messages messages={messages} />;
}

💎 revalidate 상수

fetch 함수의 재검증 설정과 동일하다. NextJS가 해당 상수를 인식하려면 반드시 export를 해야하며 반드시 revalidate라는 이름을 사용해야한다.

💎 dynamic 상수

파일 전체에서 캐싱을 전혀 하지 않도록 설정한다. 이 상수의 기본값은 auto이고 만약 이 상수의 값을 'force-dynamic'을 설정한다면 fetch 요청에서 캐시 설정을 no-store로 설정한 것과 동일하다.

💎 unstable_noStore 혹은 noStore 함수

캐시되지 않도록 하는 마지막 방법은 'next/cache'에서 unstable_noStore 함수(혹은 그저 noStore)를 사용하는 방법이다.
이 방법은 export const dynamic = 'force-dynamic'보다 권장되는 방법이다. 이 함수는 데이터가 캐시되지 않도록 확실하게 하고자 하는 컴포넌트 내에서 호출하는 것이다.
이 방법은 특정 컴포넌트의 캐싱을 비활성화할 수 있다.


📖 전체 라우트 캐시(Full Route Cache) 이해하기

전체 라우트 캐시는 빌드 시 이미 생성되고 초기화되는데 개발서버를 중단하고 npm run build를 실행해 프로덕션용으로 빌드하면 된다. 단, 동적 매개변수가 있는 동적 페이지는 예외인데 이때는 기본적으로 모든 동적 매개변수값을 모르기 때문이다.

npm start로 프로덕션 서버를 시작할 때 미리 렌더링된 페이지들이 바로 보이게 된다. 해당 페이지들은 NextJS에 의해 캐시된다.

만약 해당 페이지에서 데이터의 변화가 있다면 위에서 언급한 방법들(revalidate, dynamic, noStore)을 설정함으로써 해결할 수 있다.


📖 revalidatePathrevalidateTag를 사용한 온디맨드 캐시 무효화

💎 revalidatePath

🔗 Next.js 공식문서 : revalidatePath

revalidatePath는 캐시된 데이터를 제거하는 또다른 방법이다.

revalidate, dynamic, noStore, fetch를 이용하는 방법은 캐싱을 전적으로 비활성화하거나 특정 캐싱 시간을 설정한다는 것이다.

revalidatePath를 호출할 때는 NextJS에 지시하여 필요할때 특정 캐시 부분을 재검증한다. → 이 방법이 더 효율적일 수 있다. 왜냐하면 데이터가 변경되었다는 것을 알고있다면 해당 사실을 NextJS에게 알리기만 하면 되기 때문이다.

// /app/messages/new/page.js
import { redirect } from "next/navigation";

import { addMessage } from "@/lib/messages";
import { revalidatePath } from "next/cache";

export default function NewMessagePage() {
  async function createMessage(formData) {
    ("use server");

    const message = formData.get("message");
    addMessage(message);
    revalidatePath("/messages");
    redirect("/messages");
  }

  return (
    <>
      <h2>New Message</h2>
      <form action={createMessage}>
        <p className="form-control">
          <label htmlFor="message">Your Message</label>
          <textarea id="message" name="message" required rows="5" />
        </p>

        <p className="form-actions">
          <button type="submit">Send</button>
        </p>
      </form>
    </>
  );
}
  • revalidatePath("/messages"); : 이 경로 및 경로 캐시와 관련된 모든 데이터들이 삭제 → 중첩된 경로와 중첩된 페이지들은 데이터와 라우트 캐시가 삭제되거나 재검증 되지 않는다.
    • 만약 revalidatePath('/messages', 'layout')으로 한다면 /messages의 데이터 뿐만 아니라 모든 중첩된 페이지의 캐시도 재검증하라고 명시적으로 지시하는 것이다.

💎 revalidateTag

🔗 Next.js 공식문서 : revalidateTag

데이터를 가져올 때 캐시될 데이터에 요청을 보낼 때 태그를 할당할 수 있게 한다.
이러한 태그들은 내부적으로 캐시된 데이터에 연결될 것이고 특정 태그로 revalidateTag를 호출하면 NextJS는 해당 태그가 있는 모든 캐시된 데이터를 재검증한다.

// /app/messages/page.js
const response = await fetch("http://localhost:8080/messages", {
  next: { tags: ["msg"] },
});

따라서 revalidatePath를 여러번 호출하는 대신 다른 페이지에서 동일한 태그를 한번만 사용하여 그 태그만 재검증하여 그 페이지들의 모든 캐시된 데이터를 지울 수 있다.

// /app/messages/new/page.js
import { redirect } from "next/navigation";

import { addMessage } from "@/lib/messages";
import { revalidatePath, revalidateTag } from "next/cache";

export default function NewMessagePage() {
  async function createMessage(formData) {
    ("use server");

    const message = formData.get("message");
    // addMessage(message);
    revalidateTag("msg");
    redirect("/messages");
  }

  return (
    <>
      <h2>New Message</h2>
      <form action={createMessage}>
        <p className="form-control">
          <label htmlFor="message">Your Message</label>
          <textarea id="message" name="message" required rows="5" />
        </p>

        <p className="form-actions">
          <button type="submit">Send</button>
        </p>
      </form>
    </>
  );
}

📖 커스텀 데이터 소스에 대한 요청 메모화 설정 - cache를 이용한 중복 요청 제거

만약 fetch API를 사용하지 않는 경우 캐시를 재검증하는 방법에 대해서 다룰 것이다.

// /app/messages/page.js
import Messages from "@/components/messages";
import { getMessages } from "@/lib/messages";

export default function MessagesPage() {
  // const response = await fetch("http://localhost:8080/messages", {
  //   next: { tags: ["msg"] },
  // });
  // const messages = await response.json();
  const messages = getMessages();

  if (!messages || messages.length === 0) {
    return <p>No messages found</p>;
  }

  return <Messages messages={messages} />;
}


// /app/messages/layout.js
import { getMessages } from "@/lib/messages";

export default function MessagesLayout({ children }) {
  // const response = await fetch("http://localhost:8080/messages");
  // const messages = await response.json();
  const messages = getMessages();
  const totalMessages = messages.length;

  return (
    <>
      <h1>Important Messages</h1>
      <p>{totalMessages} messages found</p>
      <hr />
      {children}
    </>
  );
}

백엔드의 로그를 봤을 때 중복 데이터 캐싱을 따로 진행하지 않음을 알 수 있다. 이는 fetch를 이용해서 데이터를 가져오지 않기 때문이다.

따라서 중복 요청 제거는 리액트의 cache 함수를 이용해 진행할 예정이다. 이 함수는 중복 요청 제거가 발생해야하는 함수를 감싸는데 사용된다.

// /lib/messages.js
import { cache } from "react";

export const getMessages = cache(function getMessages() {
  console.log("Fetching messages from db");
  return db.prepare("SELECT * FROM messages").all();
});

📖 커스텀 데이터 소스에 대한 데이터 캐싱 설정

데이터 캐싱을 설정하기 위해서 next/cache에서 unstable_cache 혹은 cache 함수를 사용한다.

// /lib/messages.js
import { cache } from "react";
import { unstable_cache as nextCache } from "next/cache";

export const getMessages = nextCache(
  cache(function getMessages() {
    console.log("Fetching messages from db");
    return db.prepare("SELECT * FROM messages").all();
  })
);

nextCachegetMessages 함수가 반환하는 데이터를 NextJS의 데이터 캐시에서 캐시할 수 있도록 한다. 이떄 이 nextCache(unstable_cache)는 항상 프로미스를 반환한다. 따라서 이를 사용하여 데이터를 불러오는 컴포넌트에 async 키워드를 추가해야한다.

// /app/messages/layout.js
import { getMessages } from "@/lib/messages";

export default async function MessagesLayout({ children }) {
  // const response = await fetch("http://localhost:8080/messages");
  // const messages = await response.json();
  const messages = await getMessages();
  const totalMessages = messages.length;

  return (
    <>
      <h1>Important Messages</h1>
      <p>{totalMessages} messages found</p>
      <hr />
      {children}
    </>
  );


// /app/messages/page.js
import Messages from "@/components/messages";
import { getMessages } from "@/lib/messages";

export default async function MessagesPage() {
  // const response = await fetch("http://localhost:8080/messages", {
  //   next: { tags: ["msg"] },
  // });
  // const messages = await response.json();
  const messages = await getMessages();

  if (!messages || messages.length === 0) {
    return <p>No messages found</p>;
  }

  return <Messages messages={messages} />;
}

}

unstable_cache 함수는 두번째 인수도 가지고 있는데 이는 캐시 키의 배열로 지정해야한다. 이는 내부적으로 캐시된 데이터를 식별하는데 사용된다.

// /lib/messages.js
import { cache } from "react";
import { unstable_cache as nextCache } from "next/cache";

export const getMessages = nextCache(
  cache(function getMessages() {
    console.log("Fetching messages from db");
    return db.prepare("SELECT * FROM messages").all();
  }),
  ["msg"]
);

📖 커스텀 데이터 소스 데이터 무효화

💎 revalidatePath 사용하기

NextJS가 공격적인 캐싱을 하기 때문에 데이터를 업데이트를 했을 때 캐시를 재검증해야한다. → 새로운 메시지를 생성할 때 revalidatePath를 사용할 수 있다.

import { redirect } from "next/navigation";

import { addMessage } from "@/lib/messages";
import { revalidatePath, revalidateTag } from "next/cache";

export default function NewMessagePage() {
  async function createMessage(formData) {
    ("use server");

    const message = formData.get("message");
    addMessage(message);
    revalidatePath("/messages"); // revalidatePath
    // revalidateTag("msg");
    redirect("/messages");
  }

  return (
    <>
      <h2>New Message</h2>
      <form action={createMessage}>
        <p className="form-control">
          <label htmlFor="message">Your Message</label>
          <textarea id="message" name="message" required rows="5" />
        </p>

        <p className="form-actions">
          <button type="submit">Send</button>
        </p>
      </form>
    </>
  );
}

💎 revalidateTag 사용하기

unstable_cache를 사용할 때, 세 번째 인자를 통해 태그를 추가할 수 있다.

// /lib/messages.js
export const getMessages = nextCache(
  cache(function getMessages() {
    console.log("Fetching messages from db");
    return db.prepare("SELECT * FROM messages").all();
  }),
  ["msg"],
  {
    tags: ["msg"],
    // revalidate: 5
  }
);

// /app/messages/new/page.js
import { redirect } from "next/navigation";

import { addMessage } from "@/lib/messages";
import { revalidatePath, revalidateTag } from "next/cache";

export default function NewMessagePage() {
  async function createMessage(formData) {
    ("use server");

    const message = formData.get("message");
    addMessage(message);
    // revalidatePath("/messages");
    revalidateTag("msg");
    redirect("/messages");
  }

  return (
    <>
      <h2>New Message</h2>
      <form action={createMessage}>
        <p className="form-control">
          <label htmlFor="message">Your Message</label>
          <textarea id="message" name="message" required rows="5" />
        </p>

        <p className="form-actions">
          <button type="submit">Send</button>
        </p>
      </form>
    </>
  );
}

0개의 댓글

관련 채용 정보