[Next js] 캐싱 원리

강동욱·2024년 7월 26일
0

들어가기 앞서

nextjs 캐싱에는 4가지의 캐싱이 있습니다.

  • Request Memoization
  • Data Cache
  • Full Route Cache
  • Route Cache

아래는 Nextjs 공식문서에서 전반적으로 어떻게 캐싱이 작동하는지 그림으로 나타낸 것입니다.

Request Memoization

요청 최적화는 다른곳에서 fetch를 사용했을 때 같은 url과 옵션을 사용하면 한번만 요청이 되는 것 입니다.

// utils.ts
const getUser = () => {
  const res = await fetch('https://user')
  
  return res.json()
}


// Component1.ts
export default function Component1() {
  const data = getUser()
}

// Component2.ts
export default function Component2() {
  const data = getUser()
}

위의 코드를 보시면 보통 우리의 생각은 getUser가 2번 실행되니 요청도 2번 갈 것이라고 생각합니다. 하지만 Nextjs 서버컴포넌트에서는 요청 최적화(Request Memoization)를 진행을 해서 요청이 한번만 실행이 됩니다. 이로 인한 이점은 data를 prop으로 전달 하지 않고 각자의 컴포넌트에서 fetch를 해도 fetch는 한번만 실행되기 때문에 prop drilling 현상을 예방할 수 있습니다.

  • route가 서버에서 렌더링이 될 때 처음 요청은 서버 인메모리에 저장되지 않고 캐싱이 Miss 됩니다.
  • fetch가 실행되서 외부에서 데이터를 가져온다음 인메모리에 저장이 됩니다.
  • 그 다음 똑같은 url의 요청이 들어오면 캐싱이 함수가 실행되지 않고 메모리에 있는 데이터를 반환합니다.
  • route가 서버에서 렌더링이 완료되면 서버에 인메모리는 리셋이 되고 요청 최적화(Request Memoization)는 초기화 됩니다.

알아두면 좋을 것들

  • 요청 최적화는 Next.js 특징이 아니라 React 특징 입니다.
  • 최적화는 GET요청에 한해서만 적용됩니다. POST, DELETE, PUT,PATCH는 적용이 안됨
  • fetch의 요청 최적화는 리액트 컴포넌트 트리에서만 적용됩니다.(generateMetadata, generateStaticParams, Layouts, Pages, ServerComponent)
  • Route Handler에서는 fetch의 요청최적화가 적용되지 않을 수 있습니다.

캐싱 기간

리액트 컴포넌트 트리가 렌더링이 끝날때 까지 입니다.

Data Cache

Next에서는 자체적으로 빌트인 데이터 캐시를 가지고 있습니다. 이것이 가능한 이유는 next에서 자체적인 fetch 기능을 구현했기 때문입니다. cache option이 클라이언트 컴포넌트에서 사용될 경우 브라우저 HTTP 캐시와 상호작용하고 서버 컴포넌트에서는 빌트인 데이터 캐시와 상호작용 합니다.

  • fetch 요청이 force-cache일 경우 데이터가 없으니까 Data Source에서 데이터를 불러온다음 Data Cache에 저장을 하고 memoization 됩니다.
  • fetch 요청이 no-store이면 fetch 결과는 항상 Data Soure에서 불러오게 되고 Data Cache에는 저장되지 않고 memoization을 진행하게 됩니다.
  • Data Cache에 저장이 되거나 안되거나에 상관 없이 리액트 렌더링이 서버에서 끝날때 까지 Request Memoization에 항상 memoization이 됩니다.

Data Cache와 Request Memoization 차이

둘의 공통점 캐시 데이터를 재사용하면서 퍼포먼스를 향상 시켜주는 것인데 차이점은 캐싱 데이터 유지기간에 있습니다. Data Cache는 새로운 요청이 들어오거나 애플리케이션이 다시 배포되더라도 계속 유지가 되는 반면 Request Memoization은 요청 생명주기에만 지속됩니다.

Memoization은 같은 리액트 컴포넌트 트리가 렌더링이 될때 렌더링 서버로부터 데이터 캐시 서버(CDN, Edge Network)나 데이터 소스(DB, CMS)까지 네트워크 사이의 요청 중복을 줄여줍니다.

Data Cache는 데이터에 직접 접근 하는 요청을 줄여줍니다.

캐싱 기간

Data Cache는 이런 방법을 선택하지 않거나 revalidate하지 않는 한 새로운 요청이 들어오거나 애플리케이션이 다시 배포되더라도 계속 유지가 됩니다.

Revalidate

revalidate하는 방법은 2가지가 있습니다.

  • next.revalidate를 이용하는 방법 (Time-Based Revalidation)
  • revalidatePath, RevalidateTag를 이용하는 방법 (On-Demand Revalidation)

next.revalidate는 fetch 함수에 옵션을 추가해주는 것과 route segment단위로 revalidate를 해줄 수 있습니다.

// fetch 함수에 옵션을 추가해서 한시간 마다 revalidate 해주는 코드
fetch('https://...', { next: { revalidate: 3600 } })
// page.tsx
// app route 기준으로 page.tsx에서 둘중 하나의 코드만 설정하면 적어주면 됩니다.
const dynamic = 'force-dynamic' 
const revalidate = 0

next.revalidate 캐싱 원리 (Time-Based Revalidation)

// 최초 요청
fetch('https://...', { next: { revalidate: 3600 } })

// 1시간 이내에 요청
fetch('https://...', { next: { revalidate: 3600 } })

// 1시간 이후에 최초 요청
fetch('https://...', { next: { revalidate: 3600 } })
  • 최초 요청의 경우에는 외부 소스에 접근하여 가져온 데이터를 Data Cache에 적용합니다.
  • 1시간 이내에 요청을 하는 경우에는 캐시된 데이터를 가져옵니다. 즉 캐시가 Hit된 상태입니다.
  • 1시간 이후에 최초 요청의 경우에는 새로운 데이터를 가져오지 않고 캐싱된 데이터를 가져옵니다. 이 과정에서 Next.js에서는 백그라운드에서 revalidation을 수행하여 Data Cache를 업데이트 합니다. revalidation이 실패하면 Data Cache는 업데이트 되지않고 이전상태를 유지합니다.

revalidatePath, RevalidateTag 원리(On-Demand Revalidation)

사용법

revalidatePath

revalidatePath에 인자로 받는 해당 라우트 세그먼트를 Server Action으로 통하여 revalidation을 진행합니다.

'use server'

import { revalidatePath } from 'next/cache'
 
export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

export default async function Component() {
  return <Form onSubmit={submit}/>
}

revalidateTag

revalidateTage에서 받은 인자와 일치한 태그 값을 가지고 있는 fetch에 한해서 Server Action을 통해서 revalidation을 진행합니다


//Component.tsx

'use server'
 
import { revalidateTag } from 'next/cache'

 
function submit() {
  await addPost()
  revalidateTag('user')
}

async function getFormData() {
  const data = fetch('https://getUser', {next:{tags: ['user'}})
}

export default async function Component() {
  const data = await getFormData()
  
  return <Form userData={data} onSubmit={submit}/>
}

설명

  • 이제는 감을 잡으셨겠지만 설명을 하자면 당연히 첫번째 요청에는 Data Cache가 Miss가되고 외부 소스에서 데이터를 가져와서 Data Cache에다가 저장합니다.
  • 특정 조건에 따라 Server Action 함수가 실행 (revalidatePath, revalidateTag 함수 실행)됩니다.
  • 이전 단계에서 실행된 revalidatePath 인자에 해당하는 라우트 세그먼트나 revalidateTag 인자와 일치한 태그값을 가진 fetch 함수가 재실행 되면 외부 소스에서 새로운 데이터를 가져와 Data Cache를 업데이트하고 업데이트 된 데이터를 받을 수 있습니다.

Time-Based Revalidation과 On-Demand Revalidation 차이

Time-Based Revalidation

On-Demand Revalidation

위 사진을 보면 아시다시피 revalidation이 일어난 이후에 최초 요청에는 Time-Based는 stale한 데이터를 받아 오고 백그라운드에서 Data Cache를 업데이트 합니다. 하지만 On-Demand 함수는 Time-Based와 달리 fresh한 데이터를 바로 받아옵니다.

Data Cache 사용하고 싶지 않을 때

// cache 옵션을 no-store로 지정해주면 됩니다. 기본값은 'force-cache'입니다.
fetch(`https://...`, { cache: 'no-store' })
// page.tsx

// 라우트 세그먼트에서는 다음과 같이 사용하면 Data Cache기능을 사용하지 않을 수 있습니다.
export const dynamic = 'force-dynamic'

Full Route Cache

Full Route Cache는 빌드타임에 라우트를 캐싱합니다. 매 요청마다 렌더링을 하는것이 아닌 이미 캐시된 라우트를 전달해줍니다. 이러한 과정을 이해하기 위해서는 우선 리액트가 렌더링을 어떻게 다루는지 그리고 Nextjs에서는 캐시를 어떻게 진행하는지 과정을 설명하겠습니다.

컴포넌트 렌더링 과정

1. 서버에서 리액트가 렌더링이 됩니다.

서버에서 Next.js는 React의 API를 사용하여 렌더링을 합니다. 렌더링 작업은 개별 경로 세그먼트와 Suspense 경계로 나누어집니다.

각각의 경계에서는 2가지 단계로 작업이 이뤄집니다

  1. RSC(React Server Component)는 스트리밍에 최적화된 RSC payload라고 불리는 데이터포맷으로 렌더링 됩니다.
  2. Next에서는 HTML을 렌더링하기위해서 RSC Payload와 서버에서는 Client Component가 렌더링이 안되기 때문에 Client Component의 위치를 알려주는 JS instruction을 이용합니다.

위와 같은 단계는 각각의 Chunk에서 실행됩니다. 다른 Chunk의 렌더링이 끝나지 않아도 기다릴 필요없이 스트리밍을 합니다.

2. 서버에서 nextjs가 캐싱을 합니다.

Next는 서버에서 렌더링된 라우트의 결과물을 캐싱해놓습니다. 이런 동작은 정적라우트나 revalidation일때 적용됩니다.

3. 클라이언트에서 Hydration과 Reconciliation이 진행됩니다.

서버에서 렌더링된 라우트가 클라이언트로 전송되면 다음과 같은 과정을 거치게 됩니다.

  1. HTML은 non-interective인 클라이언트와 서버 컴포넌트가 보여줍니다.
  2. RSC Payload는 클라이언트 컴포넌트를 재조정(reconcil)하고 서버 컴포넌트 트리를 렌더링하고 DOM을 업데이트 합니다.
  3. RSC Payload가 가지고 있는 JS Instruction은 클라이언트 컴포넌트를 Hydration을 진행시키고 어플리케이션을 interective하게 만듭니다.

4. Next에서 Route Cache를 진행합니다.

RSC payload는 Route Cache에 저장됩니다. Full route Cache랑 다른점은 클라이언트 인메모리 캐시입니다. Route Cache를 사용하면 페이지간의 Navigation이 일어날때 이전 갔던 라우트를 캐싱할 수 있고 이후에 갈 라우트를 미리 불러 올 수 있습니다. Route Cache에 라우트가 저장되어져 있으면 서버에 요청을 생략하고 즉시 라우트를 렌더링할 수 있습니다.

캐싱 기간

기본값으로는 캐싱은 계속 지속이 됩니다.

캐싱 무효화 방법

  • Data Cache에 Time-Based revalidation, On-Demand Revalidation을 진행하면 된다.
  • Data Cache와 달리 Full Route Cache는 재배포시 삭제되어집니다.

Full Route Cahche 선택하지 않는 방법

  • DynamicFuntion(cookies, headers)를 이용해서 선택하지 않을 수 있다. 단 Data Cache는 여전히 적용됩니다.
  • 라우트 세그먼트 설정을dynamic = 'force-dynamic' 또는 revalidate = 0함으로써 Full Route Caching을 선택하지 않을 수 있습니다. 이와 같은 설정은 들어오는 요청마다 서버에다가 요청을 보내 데이터를 불러옵니다. 단 Route Cache는 클라이언트에서 이뤄지기 때문에 여전히 저장됩니다.
  • Data-Cache를 선택 안하는 방법을 사용하면됩니다.

Data-Cache를 선택하지 않았을때는 하이브리드 캐싱이 진행될 수 있는데 즉 캐싱된 데이터와 캐싱되지 않는 데이터를 같이 사용할 수 있습니다.

// ComponentA.tsx

export default function ComponentA () {
  const data = fetch(`https://...`, { cache: 'no-store' })
  const data2 = fetch(`https://...`, { cache: 'force-cache' })
}

Router Cache

Next에서는 클라이언트에서 RSC Payload와 Route Segment를 캐싱할 수 있는 인메모리를 가지고 있습니다. Link 컴포넌트에 기반하여 캐싱이 작용되고 리액트와 브라우저 상태가 그대로 보존됩니다.

Full Route Cache와 Router Cache의 차이점

Router Cache는 사용자 세션 동안 브라우저에 React 서버 컴포넌트 페이로드를 일시적으로 저장합니다.
Full Route Cache는 React 서버 컴포넌트 페이로드와 HTML을 여러 사용자 요청에 걸쳐 서버에 지속적으로 저장합니다.

Full Router Cache는 정적인 라우트만 캐싱되는 반면 Route Cache는 정적인 라우트, 동적인 라우트 둘다 캐싱을 합니다.

캐싱 기간

Router Cache를 지속하는데 2가지 요인이 있습니다.

  • Session: 페이지 네비게이션이 되는동안 지속적으로 유지합니다. 하지만 페이지가 새로고침되면 캐시는 삭제가 됩니다.
  • Automatic Invalidation Period: 각각의 라우트 세그먼트 캐시들은 일정 시간이 지나면 자동적으로 캐시가 무효화가 됩니다. Link 컴포넌트의 prefetch prop의 값에 따라 달라집니다
    - prefetch={null}이거나 설정을 안했을 경우에는 30초동안 유지가 됩니다.
    • prefetch={true}이거나 router.prefetch일 경우 5분동안 캐시가 유지됩니다.

Router Cache 캐싱 무효화

  • revalidatePath,revalidateTag를 사용(On-Demand revalidation)
  • cookies.set,cookiest.delete를 사용
  • router.refresh를 사용

Cache 메커니즘

Data Cache와 Full Route Cache의 상호작용

  • Data Cache를 revalidate했을 때와 선택하지 않았을 때는 Full Route 캐시도 무효화 됩니다.
  • 역으로 Full Route를 선택하지 않거나 무효화를 진행했을 때는 Data Cache는 영향을 받지 않습니다. 그러므로 동적 라우트를 진행했을때 캐싱된 데이터 또는 캐싱되지 않는 데이터 2가지를 효율적으로 사용할 수 있습니다.

Data Cache와 Client-side Router Cache의 상호작용

Router Cache가 hard refresh나 revlidate 기간이 경과할 때까지 이전의 페이로드 데이터를 계속 제공합니다. 데이터 캐시와 라우터 캐시를 즉시 무효화하려면 서버 액션(Server Action)에서 revalidatePath 또는 revalidateTag를 사용할 수 있습니다.

출처: Nextjs 공식홈페이지

profile
차근차근 개발자

0개의 댓글