1. Fetching

개인화되지 않은 요청

  • fetch의 기본 캐시 옵션은 force-cache로 요청의 응답이 원격 서버에 캐싱됩니다
    • force-cache는 개인화되지 않고 GET 메서드로 요청할 때에만 사용합니다.

개인화된 요청

‼ 기존에는 tanstack-query를 사용하여 client-side에서 백엔드 API 호출 시 사용자의 네트워크 비용이 발생했습니다. 그러나 서버 컴포넌트에서 fetch를 사용한 요청은 프런트 서버 비용이 발생합니다. 캐시 관리를 효율적으로 하지 않으면, 불필요한 프론트 서버 비용과 백엔드 API 호출 비용이 동시에 발생할 수 있습니다.

 


 

2. Caching

Data Cache

💡 Next.js 에서는 서버 요청 및 배포 전반에 걸쳐 fetch 요청에 대한 결과를 유지하는 데이터 캐시가 내장되어 있습니다.
이 데이터 캐시는 클라이언트 캐시가 아닌 서버 캐시를 의미합니다.

 

 

Duration

fetch의 캐시 옵션을 force-cache로 설정 후 배포하게 되면, revalidate 하거나 opt-out 하지 않는 이상 모든 배포에서 캐싱된 데이터가 유지됩니다.

  • Vercel에서 Project 캐시를 삭제하는 방법

Vercel Purge Cache
Purge Everything 버튼을 클릭해 캐시된 데이터를 삭제할 수 있습니다. 하지만 적절한 갱신 방법을 통해 API를 갱신하는 것이 바람직합니다.

 

Revalidating

revalidate 요청이 들어오면 Vercel 인프라의 게이트웨이에서 서버리스 함수를 호출하여 revalidate 후 새로운 데이터를 반환합니다.

 

Time-Based

revalidate 주기를 설정하여 API를 백그라운드에서 revalidate 시킬 수 있습니다.

// Revalidate at most every hour
  fetch('https://...', { next: { revalidate: 3600 } })
  

 

On-Demand

  • revalidateTag를 이용하여 사용자의 동작에 따라 API를 백그라운드에서 revalidate 할 수 있습니다.
fetch(
    `https://.../v1/individual_inquiry/?${individualInquiryQueryParams?.toString()}`,
    {
      next: {
        revalidate: 60,
        // fetch tags에 api 고유의 키 + query params 조합의 키 추가
        tags: [FETCH_TAGS_INDIVIDUAL_INQUIRY_API.LIST,`${
            FETCH_TAGS_INDIVIDUAL_INQUIRY_API.LIST
          }${individualInquiryQueryParams?.toString()}`],
      },
    },
  );
  • FETCH_TAGS_INDIVIDUAL_INQUIRY_API.LIST가 삽입된 모든 API revalidate
revalidateTag(FETCH_TAGS_INDIVIDUAL_INQUIRY_API.LIST)

 

 

  • FETCH_TAGS_INDIVIDUAL_INQUIRY_API.LIST + query params 가 삽입된 API revalidate
revalidateTag(`${FETCH_TAGS_INDIVIDUAL_INQUIRY_API.LIST
  }${individualInquiryQueryParams?.toString()}`)
  


  • revalidatePath를 이용하여 해당 경로의 데이터 캐시와, 경로 캐시를 revalidate 할 수 있습니다.

revalidatePath('/ko');
  


  • route handlers를 생성하고 API 갱신하는 로직을 구현합니다.
  • CMS에서 새로운 콘텐츠가 업로드될 때 route API(ex. {FRONT_DOMAIN}/api/revalidate/{revalidateName})를 호출하여 데이터를 갱신합니다.
  • 해당 방법은 읽기 요청이 쓰기(갱신 후 data cache set)요청보다 많을 때 api 요청 비용이 효과적으로 절감됩니다.

 

import { revalidateTag } from 'next/cache';
  import { NextRequest } from 'next/server';

  import { ENV } from '@/configs/env';

  const API_KEY = ENV['X-API-KEY'];

  async function invalidateCache(revalidateName: string, id: string | null) {
    if (id) {
      revalidateTag(`${revalidateName}${id}`);
      revalidateTag(revalidateName);
    } else {
      revalidateTag(revalidateName);
    }

    return new Response(JSON.stringify({}), {
      status: 200,
      statusText: 'success',
    });
  }

  async function invalidRequest() {
    return new Response(JSON.stringify({}), {
      status: 401,
      statusText: 'unauthorized',
    });
  }

  export async function DELETE(
    req: NextRequest,
    {
      params,
    }: {
      params: {
        revalidateName: string;
      };
    },
  ) {
    const isValidApiKey = req.headers.get('X-API-KEY') === API_KEY;

    if (!isValidApiKey) {
      return invalidRequest();
    }

    const { revalidateName } = params;

    const searchParams = new URLSearchParams(req.nextUrl.searchParams);
    const id = searchParams.get('id');

    try {
      return invalidateCache(revalidateName, id);
    } catch (error) {
      return new Response(JSON.stringify({}), {
        status: 500,
        statusText: 'internal server error',
      });
    }
  }

 

Opting out

 

  • 요청 단위의 데이터 캐시 해제
// Opt out of caching for an individual `fetch` request
  fetch(`https://...`, { cache: 'no-store' })
  
  • 경로 단위의 모든 요청 데이터 캐시 해제
// Opt out of caching for all data requests in the route segment
  export const dynamic = 'force-dynamic'
  

 

Request Memoization

  • fetch를 사용할 때 동일한 url & option의 Request를 자동으로 메모합니다.
  • layout page component generateMetadata generateStaticParams에서 같은 요청 시 첫 번째 Request에서 캐싱되고, 후속 Request 시 함수를 실행하지 않고 메모리에서 반환합니다.
  • fetch를 사용한 Request Memoization은 GET method에서만 적용됩니다.
// a
  <Suspense fallback={...}>
      <CelebSection
        data={celebList({
          query: celebQueryParams,
        })}
      />
  </Suspense>
        
  // a
  <Suspense fallback={...}>
      <CelebSection
        data={celebList({
          query: celebQueryParams,
        })}
      />
  </Suspense>
        
  // a-1
  <Suspense fallback={...}>
      <CelebSection
          data={celebList({
            query: celebQueryParams1,
          })}
      />
  </Suspense>

같은 경로에서 a api 2번 호출, query params가 다른 a-1 api 1번 호출

Duration

요청 메모는 서버 요청 간에 공유되지 않고 React 컴포넌트 트리 안에서 렌더링 중에만 적용되므로 재검증할 필요가 없습니다.

Opting out

fetch를 사용한 요청에서 아래의 방법으로 요청 메모를 해제할 수 있습니다.

const { signal } = new AbortController()
  fetch(url, { signal })
  

+ Recent posts