[번역] 캐싱

Next.js
2024-03-12

원글 : https://nextjs.org/docs/app/building-your-application/caching

Next.js는 렌더링 작업과 데이터 요청을 캐싱하여 어플리케이션의 성능을 향상시키고 비용을 감소시킨다.

Overview

메커니즘캐싱위치목적지속 기간
요청 메모함수의 반환 값서버리액트 컴포넌트 트리에서 데이터 재사용요청별 생명주기
데이터 캐시데이터서버유저 요청과 배포 간에 데이터 저장영구(재검증 가능)
전체 라우트 캐시HTML과 RSC Payload서버렌더링 과 성능 향상영구(재검증 가능)
라우터 캐시RSC Payload클라이언트탐색에서 서버 요청 감소유저 세션 or 시간기반

기본적으로 Next.js는 성능을 향상시키고 비용을 줄이기 위해 가능한 한 캐시한다. 즉, 설정 해제를 하지 않는 한 라우트는 정적으로 렌더링되고 데이터 요청은 캐시된다.

다음 다이어그램은 빌드 시 라우트가 정적으로 렌더링될 때와 정적 라우트가 처음 액세스될 때의 기본 캐시 동작을 보여준다.

image

캐시 동작은 경로가 정적으로 렌더링되거나 동적으로 렌더링되거나, 데이터가 캐시되거나 캐시되지 않거나, 요청이 첫 번째 방문의 일부인지, 후속 탐색의 일부인지에 따라 다르다. 사용 사례에 따라 개별 라우트 및 데이터 요청의 캐시 동작을 구성할 수 있습니다.

요청 메모

리액트는 fetch API를 확장하여 동일한 URL과 옵션을 가진 요청을 자동으로 기록합니다. 즉, 리액트 컴포넌트 트리의 여러 위치에서 동일한 데이터의 fetch 함수를 한 번만 실행하면서 호출할 수 있음을 의미한다.

image

예를 들어 전체 라우트(Layout, Page, 여러 컴포넌트 등)에서 동일한 데이터를 사용해야 하는 경우 트리의 최상위 수준에서 데이터를 가져온 다음, 컴포넌트 간에 props를 전송할 필요가 없다. 대신 동일한 데이터에 대해 네트워크에서 여러 요청을 수행하여 성능에 영향을 주지 않고 필요한 컴포넌트로 데이터를 가져올 수 있습니다.

async function getItem() { // `fetch` 함수는 자동으로 기록되고 결과는 캐시된다. const res = await fetch('https://.../item/1') return res.json() } // 이 함수는 2번 호출되지만, 처음에만 실행된다. const item = await getItem() // cache MISS // 이 2번째 호출은 라우트 어디에서나 사용할 수 있다.(라우트 어디에서나 캐시가 히트됨) // The second call could be anywhere in your route const item = await getItem() // cache HIT
async function getItem() { // `fetch` 함수는 자동으로 기록되고 결과는 캐시된다. const res = await fetch('https://.../item/1') return res.json() } // 이 함수는 2번 호출되지만, 처음에만 실행된다. const item = await getItem() // cache MISS // 이 2번째 호출은 라우트 어디에서나 사용할 수 있다.(라우트 어디에서나 캐시가 히트됨) // The second call could be anywhere in your route const item = await getItem() // cache HIT

메모 요청 작동 방식

image

  • 라우트를 렌더링하는 동안, 특정 요청이 처음 호출되면, 결과는 메모리에 없으며 캐시 미스가 된다.
  • 따라서 함수가 실행되고, 외부 소스에서 데이터가 fetch되고, 결과가 메모리에 저장됩니다.
  • 동일한 렌더링 패스에서 요청의 후속 함수 호출은 캐시 히트가 되며, 함수는 실행되지 않고 데이터가 메모리에서 반환된다.
  • 라우트가 렌더링되고 렌더링 패스가 완료되면, 메모리가 "재설정"되고 모든 요청의 메모화 항목이 지워집니다.

렌더링 패스 : 렌더링 엔진이 웹 페이지의 HTML, CSS, JavaScript를 처리하고 최종적으로 사용자가 볼 수 있는 화면을 생성하는 각각의 단계 (e.g. DOM 트리 구축, CSSOM 트리 구축, 레이아웃, 페인팅 등)

요청 메모이제이션은 리액트 기능이다. 다른 캐싱 메커니즘과 어떻게 상호작용하는지 보여주기 위해 여기에 포함된다.

메모이제이션은 fetch 요청에서 GET 메소드에서만 적용된다.

메모이제이션은 리액트 컴포넌트 트리에서만 적용된다. generateMetadata, staticparams, Layouts, Pages 및 기타 Server Components의 fetch 요청에 적용된다. Route Handlers의 fetch 요청은 리액트 컴포넌트 트리의 일부가 아니기 때문에 해당되지 않습니다.

지속

캐시는 리액트 컴포넌트 트리 렌더링이 완료될 때까지 서버 요청의 수명 동안 유지됩니다.

재검증

메모이제이션은 서버 요청 간에 공유되지 않고, 오직 렌더링할 때만 적용되므로, 재검증할 필요가 없다.

데이터 캐시

Next.js에는 들어오는 서버 요청과 배포 전반에 걸쳐 데이터 fetches의 결과를 유지하는 데이터 캐시가 포함되어 있습니다. Next.js가 네이티브 fetch API를 확장하여 서버의 각 요청이 자체 영구 캐시 의미를 설정할 수 있도록 합니다.

Next.js는 들어오는 서버 요청과 배포 전반에 걸쳐 데이터 fetches의 결과를 유지하는 빌트인 데이터 캐시를 가지고 있다. 이는 next.js가 네이티브 fetch API를 확장하여 서버의 각 요청이 고유한 영구 캐싱 semantics를 설정할 수 있도록 하기 때문에 가능하다.

알아야 할 사항: 브라우저에서 fetchcache 옵션은 요청이 브라우저의 HTTP 캐시와 상호 작용하는 방식을 나타내고, Next.js에서 cache 옵션은 서버측 요청이 서버 Data Cache와 상호 작용하는 방식을 나타냅니다.

기본적으로 fetch를 사용하는 데이터 요청은 캐시됩니다. fetchcachenext.revalid 옵션을 사용하여 캐시 동작을 구성할 수 있습니다.

데이터 캐시 동작 방식

image

  • 렌더링 중에 fetch 요청이 처음 호출되면, Next.js는 데이터 캐시에서 캐시된 응답을 확인한다.
  • 캐시된 응답이 발견되면, 즉시 반환되고 메모된다.
  • 캐시된 응답을 찾을 수 없는 경우, 데이터 소스에 요청하고, 그 결과가 데이터 캐시에 저장되며 메모화된다.
  • 캐시되지 않은 데이터(e.g. {cache: ‘no-store’})의 경우 항상 데이터 소스에서 결과를 가져와 메모화한다.
  • 데이터가 캐시되는지 여부에 관계없이 리액트 렌더 패스 중에는 동일한 데이터에 대해 중복 요청이 발생하지 않도록 요청이 항상 기록됩니다.

데이터 캐시와 요청 메모이제이션의 차이점

두 캐싱 메커니즘 모두 캐시된 데이터의 재사용으로 성능을 항샹시키는 데 도움이 되지만, 데이터 캐시는 들어오는 요청 및 배포 전반에 걸쳐 지속적으로 유지되는 반면에 메모화는 요청의 수명 동안애만 지속된다.

메모이제이션을 사용하면, 렌더링 서버에서 데이터 캐시 서버(예: CDN 및 Edge Networ) 또는 데이터 소스(e.g. 데이터베이스 또는 CMS)까지 네트워크 경계를 넘어서야 하는 동일한 렌더링 패스에서 중복 요청 수를 줄입니다.

데이터 캐시를 사용하면, 원본 데이터 소스로 요청하는 수가 줄어든다.

지속

데이터 캐시는 재검증 또는 설정 해제를 하지 않는 한 들어오는 요청 및 배포 전반에 걸쳐 지속됩니다.

재검증

  • 시간 기반 재검증: 일정 시간이 경과하여 새 요청이 발생한 후 데이터를 재검증한다. 이는 자주 변경되지 않으며 데이터가 최신일 필요가 없는 데이터에 유용하다.
  • 요구 기반 재검증 : 이벤트(예: form 제출)를 기반으로 데이터를 재검증한다. On-demand revalidation은 tag기반 혹은 path 기반 접근 방식을 사용하여 데이터 그룹을 한 번에 재검증할 수 있다. 이 방법은 최신 데이터가 가능한 한 빨리 표시되도록 할 때 유용하다. (예: 헤드리스 CMS의 컨텐츠가 업데이트된 경우)

시간 기반 재검증 image

  • revalidate가 있는 fetch 요청이 처음 호출되면 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장한다.
  • 지정된 기간(예: 60초) 내에 호출된 모든 요청은 캐시된 데이터를 반환한다.
  • 기간이 지난 후에도 다음 요청은 캐시된(이제는 오래된) 데이터를 반환한다.
    • Next.js는 백그라운드에서 데이터의 재검증을 트리거한다.
    • 데이터가 성공적으로 fetch되면, Next.js가 데이터 캐시를 새 데이터로 업데이트한다.
    • 백그라운드 재검증에 실패하면 이전 데이터가 변경되지 않고 유지된다.

요구 기반 재검증 image

  • fetch 요청을 처음 호출하면 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장한다.
  • 요구 기반 재검증이 트리거되면 해당 캐시 항목이 캐시에서 삭제됩니다.
    • 이는 새로운 데이터를 가져올 때까지 오래된 데이터를 캐시에 보관하는 시간 기반 재검증과는 다릅니다.
  • 다음에 요청이 이루어지면, 다시 캐시 미스가 되며 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.

전체 라우트 캐시(Full Route Cache)

관련 용어 : 자동 정적 최적화, SSG 또는 정적 렌더링이라는 용어는 빌드 시 애플리케이션의 라우트를 렌더링하고 캐시하는 프로세스를 가리키는 데 동일한 의미로 사용될 수 있다.

Next.js는 빌드 시 자동으로 라우트를 렌더링하고 캐시한다. 이는 각 요청에 대해 서버에서 렌더링하는 것이 아니라 캐시된 라우트를 제공할 수 있도록 하는 최적화로 페이지 로드 속도가 빨라진다.

전체 라우트 캐시의 작동 방식을 이해하려면 어떻게 리액트가 렌더링을 처리하는 지와 어떻게 Next.js가 결과를 캐시하는 지를 확인하면 도움이 된다.

1. 서버에서 리액트 렌더링

서버에서 Next.js는 리액트 API를 사용하여 렌더링을 조정합니다. 렌더링 작업은 개별 라우트 부분과 Suspense boundaries에 따라 청크로 나뉩니다.

각 청크는 두 단계로 렌더링됩니다:

  1. 리액트는 서버 컴포넌트를 스트리밍에 최적화된 특수 데이터 형식인 RSC Payload로 렌더링한다.
  2. Next.js는 서버에서 HTML을 렌더링하기 위해 RSC Payload 및 클라이언트 컴포넌트 자바스크립트 명령을 사용한다.

이는 작업을 캐시하거나 응답을 보내기 전에 모든 것이 렌더링될 때까지 기다릴 필요가 없음을 의미한다. 대신 작업이 완료되면 응답을 스트리밍할 수 있습니다.

2. 서버에서 Next.js 캐싱 (전체 라우트 캐시)

image

Next.js의 기본 동작은 라우트의 렌더링 결과(RSC Payload 및 HTML)를 서버에 캐시하는 것이다. 이는 빌드 시 또는 재검증 중에 정적으로 렌더링된 경로에 적용된다.

3. 클라이언트에서 하이드레이션 및 재조정

요청시 클라이언트에서 다음을 수행한다.

  1. HTML은 클라이언트 및 서버 컴포넌트의 빠른 non-interactive 초기 미리보기를 즉시 표시하는 데 사용된다.
  2. RSC Payload는 클라이언트 트리와 렌더링된 서버 컴포넌트 트리를 조정하고 DOM을 업데이트하는 데 사용된다.
  3. 자바스크립트 명령은 클라이언트 컴포넌트를 hydrate하고 애플리케이션을 인터렉티브하게 만드는 데 사용된다.

4. 클라이언트에서 Next.js 캐싱 (라우터 캐시)

RSC Payload는 클라이언트 측 라우터 캐시에 저장된다. 이 라우터 캐시는 이전에 방문한 라우트를 저장하고 향후 라우트를 prefetching하여 탐색 경험을 개선하는 데 사용됩니다.

5. 후속 탐색

후속 탐색 시 또는 prefetching 중에 Next.js는 RSC Payload가 라우터 캐시에 저장되어 있는지 확인합니다. 이 경우 새 요청 전송은 건너뛴다.

라우트 segments가 캐시에 없으면 Next.js는 서버에서 RSC Payload를 가져오고 클라이언트의 라우터 캐시에 데이터를 추가한다.

정적 렌더링 및 동적 렌더링

라우트가 빌드 시 캐시되는지 여부는 라우트가 정적으로 렌더링되는지 혹은 동적으로 렌더링되는지에 따라 결정된다. 정적 라우트는 기본적으로 캐시되지만 동적 라우트는 요청 시 렌더링되며 캐시되지 않는다.

이 그림은 캐시된 데이터와 캐시되지 않은 데이터를 사용하여 정적으로 렌더링된 경로와 동적으로 렌더링된 경로의 차이를 보여준다.

image

지속

기본적으로 전체 라우트 캐시는 영구적이며, 이는 렌더링 출력은 사용자의 요청에 걸쳐 캐시됨을 의미한다.

무효

전체 캐시 라우트를 무효할 수 있는 방법은 두 가지이다.

  1. 데이터 재검증: 데이터 캐시를 재검증하면, 서버에서 컴포넌트가 다시 렌더링되고 새 렌더링 출력이 캐시되므로 라우터 캐시가 무효된다.
  2. 재배포: 모든 배포 간에 유지되는 데이터 캐시와 달리 전체 라우트 캐시는 새 배포로 지워진다.

적용 해제

  • 동적 함수 사용: 이렇게 하면 전체 라우트 캐시에서 라우트가 적용 해제되고 요청 시 동적으로 렌더링된다. 데이터 캐시는 계속 사용할 수 있다.
  • dynamic = 'force-dynamic' 또는 revalidate = 0 라우트 segment 구성 옵션 사용: 이렇게 하면 전체 라우트 캐시와 데이터 캐시를 건너뛴다. 즉, 서버에 대한 각 수신 요청에 대해 컴포넌트가 렌더링되고 데이터가 fetch된다. 라우터 캐시는 클라이언트 측 캐시이므로 여전히 적용된다.
  • 데이터 캐시 적용 해제: 라우트에 캐시되지 않은 fetch 요청이 있으면 전체 라우트 캐시에서 라우트를 적용 해제한다. 특정 fetch 요청의 데이터는 각 수신 요청에 대해 fetch된다. 적용 해제하지 않는 다른 fetch 요청은 여전히 데이터 캐시에서 캐시된다. 이렇게 하면 캐시된 데이터와 캐시되지 않은 데이터 간의 하이브리드가 가능하다.

라우터 캐시(Router Cache)

관련 용어:

라우터 캐시를 클라이언트 쪽 캐시 또는 prefetch 캐시라고 한다. prefetch 캐시는 prefecth된 라우트 segments를 가리키지만, 클라이언트 측 캐시는 방문한 세그먼트와 prefetch된 세그먼트를 모두 포함하는 전체 라우터 캐시를 가리킨다. 이 캐시는 특히 Next.js 및 서버 컴포넌트에 적용되며 브라우저의 bfcache와는 다르지만 유사한 결과를 얻을 수 있다.

Next.js에는 사용자 세션 동안 개별 라우트 세그먼트로 구분된 RSC Payload를 저장하는 인메모리 클라이언트 측 캐시가 있다. 이를 라우터 캐시라고 합니다.

라우터 캐시 동작 방식

image

사용자가 라우트를 탐색하면, Next.js는 방문한 라우트 세그먼트를 캐시하고 사용자가 탐색할 가능성이 있는 라우트(뷰포트의 <Link> 컴포넌트 기반의)를 prefetch합니다.

이렇게 하면 사용자의 탐색 경험이 향상된다.

  • 방문한 라우트가 캐시되므로 즉각적인 전/후 탐색과 prefetching과 부분 렌더링에 의해 새로운 라우트에서의 빠른 탐색
  • 탐색과 리액트 상태 사이에 전체 페이지가 다시 로드되지 않고 브라우저 상태가 유지된다.

라우터 캐시와 전체 경로 캐시의 차이:

라우터 캐시는 사용자 세션 동안 RSC Payload를 브라우저에 일시적으로 저장하지만, 전체 경로 캐시는 여러 사용자 요청에 걸쳐 RSC Payload 및 HTML을 서버에 지속적으로 저장한다.

전체 경로 캐시는 정적으로 렌더링된 경로만 캐시하는 반면, 라우터 캐시는 정적으로 렌더링된 경로와 동적으로 렌더링된 경로 모두에 적용된다.

지속

캐시는 브라우저의 임시 메모리에 저장된다. 라우터 캐시의 지속 기간은 다음 두 가지 요소에 의해 결정된다.

  1. 세션: 캐시는 탐색 전체에서 유지된다. 그러나 페이지를 새로고침하면 지워진다.
  2. 자동 무효화 기간: 개별 세그먼트의 캐시는 특정 시간이 지나면 자동으로 무효화된다. 지속 기간은 경로가 정적으로 렌더링되는지 동적으로 렌더링되는지에 따라 달라진다.
    1. 동적 렌더링: 30초
    2. 정적 렌더링: 5분

페이지를 새로 고침하면 캐시된 모든 세그먼트가 지워지지만, 자동 무효화 기간은 마지막으로 액세스하거나 만든 시점의 개별 세그먼트에만 영향을 미친다.

prefetch={true}를 추가하거나 동적으로 렌더링된 경로에 대해 router.prefetch를 호출하여 5분캐싱을 선택할 수 있다.

무효화

라우터 캐시를 무효화할 수 있는 방법은 두 가지입니다:

  • Server Action의 경우:
    • (revalidatePath)를 사용하거나 (revalidateTag)를 사용하여 캐시 태그를 사용하여 요구 기반 데이터의 재검증
    • cookie.set 또는 cookie.delete를 사용하면 라우터 캐시가 무효화되고 쿠키를 사용하는 라우트가 오래되지 않도록 방지한다. (e.g. 인증)
  • router.refresh를 호출하면 라우터 캐시가 무효화되고 서버에 현재 경로를 새로 요청합니다.

적용 해제

라우터 캐시를 사용하지 않는 것은 불가능하다.

<Link> 컴포넌트의 prefetch prop을 false로 설정하여 prefetch를 해제할 수 있습니다. 그러나 라우트 세그먼트는 30초 동안 일시적으로 저장되므로 탭 막대와 같은 중첩 세그먼트 간의 즉각 탐색 및 앞뒤 탐색이 가능합니다. 방문한 경로는 계속 캐시됩니다.

캐시 상호작용

다양한 캐싱 메커니즘을 구성할 때는 서로 상호 작용하는 방식을 이해하는 것이 중요합니다:

데이터 캐시와 전체 라우트 캐시

  • 렌더링 출력은 데이터에 따라 다르므로, 데이터 캐시를 재검증하거나 적용 해제하면 전체 라우트 캐시가 무효화된다.
  • 전체 라우트 캐시를 무효화하거나 적용 해제해도 데이터 캐시에 영향을 주지 않습니다. 캐시된 데이터와 캐시되지 않은 데이터를 모두 포함하는 라우트를 동적으로 렌더링할 수 있다. 이는 대부분의 페이지가 캐시된 데이터를 사용하고 요청 시 fetch해야 하는 데이터에 의존하는 일부 컴포넌트가 있는 경우 유용하다. 모든 데이터를 re-fetching할 때의 성능 영향에 대한 걱정 없이 동적으로 렌더링할 수 있다.

데이터 캐시와 클라이언트 측 라우터 캐시

라우트 핸들러가 특정 라우트에 연결되어 있지 않으므로 라우트 핸들러에서 데이터를 재검증해도 즉시 라우터 캐시를 무효화 하지 않는다. 이는 하드 새로고침이나 자동 무효화 기간이 경과할 때까지 라우터 캐시가 계속해서 이전 페이로드를 제공함을 의미한다.. 데이터 캐시 및 라우터 캐시를 즉시 무효화 하려면, Server Action에서 revalidatePath 혹은 revalidateTag 를 사용할 수 있다.

관련 포스트

post thumbnail

[번역] Next.js 14

Next.js 14는 다음과 같은 가장 중점으로 둔 릴리스이다. - Turbopack: App & Pages Router에서 5,000번의 테스트 통과 - 로컬 서버 시작이 53%

2024-03-12
post thumbnail

[번역] 구성 패턴

리액트 애플리케이션을 빌드할 때, 애플리케이션의 어느 부분을 서버 또는 클라이언트에서 렌더링할지 고려해야 한다. 이 글은 서버 및 클라이언트 컴포넌트를 사용할 때 권장되는 몇 가지

2024-03-11
post thumbnail

[번역] 클라이언트 컴포넌트

클라이언트 컴포넌트는 요청 시 클라이언트에서 렌더링할 수 있는 인터랙티브한 UI를 만들 수 있게 해준다. Next.js에서 클라이언트 렌더링은 opt-in으로, 리액트가 클라이언트

2024-03-11
post thumbnail

[번역] 서버 컴포넌트

리액트 서버 컴포넌트는 서버에서 렌더링되고 선택적으로 캐시된 UI를 만들 수 있게 한다. Next.js에서 렌더링 하는 것은 route segments에 의해 더 나누어져있어서 s

2024-03-11

도로모의 기술 블로그

# Contact : jyw966@naver.com

Copyright © doromo. All Rights Reserved.