nextjs 캐싱에는 4가지의 캐싱이 있습니다.
아래는 Nextjs 공식문서에서 전반적으로 어떻게 캐싱이 작동하는지 그림으로 나타낸 것입니다.
요청 최적화는 다른곳에서 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 현상을 예방할 수 있습니다.
리액트 컴포넌트 트리가 렌더링이 끝날때 까지 입니다.
Next에서는 자체적으로 빌트인 데이터 캐시를 가지고 있습니다. 이것이 가능한 이유는 next에서 자체적인 fetch 기능을 구현했기 때문입니다. cache option이 클라이언트 컴포넌트에서 사용될 경우 브라우저 HTTP 캐시와 상호작용하고 서버 컴포넌트에서는 빌트인 데이터 캐시와 상호작용 합니다.
둘의 공통점 캐시 데이터를 재사용하면서 퍼포먼스를 향상 시켜주는 것인데 차이점은 캐싱 데이터 유지기간에 있습니다. Data Cache는 새로운 요청이 들어오거나 애플리케이션이 다시 배포되더라도 계속 유지가 되는 반면 Request Memoization은 요청 생명주기에만 지속됩니다.
Memoization은 같은 리액트 컴포넌트 트리가 렌더링이 될때 렌더링 서버로부터 데이터 캐시 서버(CDN, Edge Network)나 데이터 소스(DB, CMS)까지 네트워크 사이의 요청 중복을 줄여줍니다.
Data Cache는 데이터에 직접 접근 하는 요청을 줄여줍니다.
Data Cache는 이런 방법을 선택하지 않거나 revalidate하지 않는 한 새로운 요청이 들어오거나 애플리케이션이 다시 배포되더라도 계속 유지가 됩니다.
revalidate하는 방법은 2가지가 있습니다.
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
// 최초 요청
fetch('https://...', { next: { revalidate: 3600 } })
// 1시간 이내에 요청
fetch('https://...', { next: { revalidate: 3600 } })
// 1시간 이후에 최초 요청
fetch('https://...', { next: { revalidate: 3600 } })
사용법
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}/>
}
설명
Time-Based Revalidation
On-Demand Revalidation
위 사진을 보면 아시다시피 revalidation이 일어난 이후에 최초 요청에는 Time-Based는 stale한 데이터를 받아 오고 백그라운드에서 Data Cache를 업데이트 합니다. 하지만 On-Demand 함수는 Time-Based와 달리 fresh한 데이터를 바로 받아옵니다.
// cache 옵션을 no-store로 지정해주면 됩니다. 기본값은 'force-cache'입니다.
fetch(`https://...`, { cache: 'no-store' })
// page.tsx
// 라우트 세그먼트에서는 다음과 같이 사용하면 Data Cache기능을 사용하지 않을 수 있습니다.
export const dynamic = 'force-dynamic'
Full Route Cache는 빌드타임에 라우트를 캐싱합니다. 매 요청마다 렌더링을 하는것이 아닌 이미 캐시된 라우트를 전달해줍니다. 이러한 과정을 이해하기 위해서는 우선 리액트가 렌더링을 어떻게 다루는지 그리고 Nextjs에서는 캐시를 어떻게 진행하는지 과정을 설명하겠습니다.
서버에서 Next.js는 React의 API를 사용하여 렌더링을 합니다. 렌더링 작업은 개별 경로 세그먼트와 Suspense 경계로 나누어집니다.
각각의 경계에서는 2가지 단계로 작업이 이뤄집니다
- RSC(React Server Component)는 스트리밍에 최적화된 RSC payload라고 불리는 데이터포맷으로 렌더링 됩니다.
- Next에서는 HTML을 렌더링하기위해서 RSC Payload와 서버에서는 Client Component가 렌더링이 안되기 때문에 Client Component의 위치를 알려주는 JS instruction을 이용합니다.
위와 같은 단계는 각각의 Chunk에서 실행됩니다. 다른 Chunk의 렌더링이 끝나지 않아도 기다릴 필요없이 스트리밍을 합니다.
Next는 서버에서 렌더링된 라우트의 결과물을 캐싱해놓습니다. 이런 동작은 정적라우트나 revalidation일때 적용됩니다.
서버에서 렌더링된 라우트가 클라이언트로 전송되면 다음과 같은 과정을 거치게 됩니다.
- HTML은 non-interective인 클라이언트와 서버 컴포넌트가 보여줍니다.
- RSC Payload는 클라이언트 컴포넌트를 재조정(reconcil)하고 서버 컴포넌트 트리를 렌더링하고 DOM을 업데이트 합니다.
- RSC Payload가 가지고 있는 JS Instruction은 클라이언트 컴포넌트를 Hydration을 진행시키고 어플리케이션을 interective하게 만듭니다.
RSC payload는 Route Cache에 저장됩니다. Full route Cache랑 다른점은 클라이언트 인메모리 캐시입니다. Route Cache를 사용하면 페이지간의 Navigation이 일어날때 이전 갔던 라우트를 캐싱할 수 있고 이후에 갈 라우트를 미리 불러 올 수 있습니다. Route Cache에 라우트가 저장되어져 있으면 서버에 요청을 생략하고 즉시 라우트를 렌더링할 수 있습니다.
기본값으로는 캐싱은 계속 지속이 됩니다.
dynamic = 'force-dynamic'
또는 revalidate = 0
함으로써 Full Route Caching을 선택하지 않을 수 있습니다. 이와 같은 설정은 들어오는 요청마다 서버에다가 요청을 보내 데이터를 불러옵니다. 단 Route Cache는 클라이언트에서 이뤄지기 때문에 여전히 저장됩니다.Data-Cache를 선택하지 않았을때는 하이브리드 캐싱이 진행될 수 있는데 즉 캐싱된 데이터와 캐싱되지 않는 데이터를 같이 사용할 수 있습니다.
// ComponentA.tsx
export default function ComponentA () {
const data = fetch(`https://...`, { cache: 'no-store' })
const data2 = fetch(`https://...`, { cache: 'force-cache' })
}
Next에서는 클라이언트에서 RSC Payload와 Route Segment를 캐싱할 수 있는 인메모리를 가지고 있습니다. Link 컴포넌트에 기반하여 캐싱이 작용되고 리액트와 브라우저 상태가 그대로 보존됩니다.
Router Cache는 사용자 세션 동안 브라우저에 React 서버 컴포넌트 페이로드를 일시적으로 저장합니다.
Full Route Cache는 React 서버 컴포넌트 페이로드와 HTML을 여러 사용자 요청에 걸쳐 서버에 지속적으로 저장합니다.
Full Router Cache는 정적인 라우트만 캐싱되는 반면 Route Cache는 정적인 라우트, 동적인 라우트 둘다 캐싱을 합니다.
Router Cache를 지속하는데 2가지 요인이 있습니다.
prefetch={null}
이거나 설정을 안했을 경우에는 30초동안 유지가 됩니다.prefetch={true}
이거나 router.prefetch
일 경우 5분동안 캐시가 유지됩니다.revalidatePath
,revalidateTag
를 사용(On-Demand revalidation)cookies.set
,cookiest.delete
를 사용router.refresh
를 사용Router Cache가 hard refresh나 revlidate 기간이 경과할 때까지 이전의 페이로드 데이터를 계속 제공합니다. 데이터 캐시와 라우터 캐시를 즉시 무효화하려면 서버 액션(Server Action)에서 revalidatePath 또는 revalidateTag를 사용할 수 있습니다.
출처: Nextjs 공식홈페이지