[NextJS] 캐싱(Caching)

우지끈·2024년 12월 11일
post-thumbnail

Next.js는 렌더링 작업과 데이터 요청을 캐싱하여 애플리케이션의 성능을 향상시키고 비용을 절감한다. 캐싱은 Next.js에서 가장 핵심적인 기능 중 하나, 이를 이해하면 Next.js 앱의 성능 최적화에 큰 도움이 된다.
위의 사진은 빌드 시정적 경로가 처음 방문될 때의 기본 캐싱 동작을 나타낸 다이어그램이다. 이를 세부적으로 살펴보겠다.


Full Route Cache

Next.js는 빌드 시점에 페이지를 렌더링하고 그 결과를 캐싱하는 Full Route Cache 기능을 제공한다. 이를 통해 서버는 매 요청마다 페이지를 다시 렌더링할 필요 없이, 미리 생성된 HTML과 데이터를 빠르게 제공할 수 있어 페이지 로딩 속도가 크게 향상된다.

작동 원리

  • React 서버 컴포넌트(RSC)를 사용하여 서버에서 데이터 렌더링
  • React Server Component Payload를 생성 -> 최종 HTML 생성
  • 이 결과물을 캐싱하여, 동일한 요청 시 다시 렌더링하지 않고 저장된 HTML을 그대로 반환
export default async function Home() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache', // 기본값 (SSG)
  });
  const data = await res.json();

  return (
    <div>
      {data.map((product) => (
        <p key={product.id}>{product.name}</p>
      ))}
    </div>
  );
}
  • cache: 'force-cache': 캐시된 데이터를 사용하며, 기본값으로 설정되어 있음(아무것도 안 적으면 force-cache)

특징

  • Full Route Cache는 지속적이기에, 페이지의 HTML 결과가 캐싱되어 동일한 요청 시 서버가 새롭게 렌더링하지 않아도 된다.

  • 다만, 데이터 변경이 자주 발생하거나 실시간 정보가 필요한 페이지에는 적합하지 않을 수 있다. -> 이를 보완하기 위해 revalidate 옵션을 사용해 주기적으로 캐시를 갱신할 수 있음


Request Memoization

Request Memoization은 서버 컴포넌트 렌더링 중 동일한 fetch 요청이 여러 번 호출되더라도, 실제로는 한 번만 실행되도록 하는 기능이다. 이를 통해 불필요한 중복 네트워크 요청을 방지하고, 서버 자원과 응답 시간을 최적화할 수 있다.

예를 들어, 컴포넌트 트리의 여러 곳에서 동일한 데이터를 필요로 할 때,

async function getCommonData() {
  const res = await fetch('<https://api.example.com/common-data>');
  return res.json();
}

// 컴포넌트 A
const dataA = await getCommonData(); // cache MISS

// 컴포넌트 B
const dataB = await getCommonData(); // cache HIT

위 코드에서 getCommonData 함수는 두 번 호출되었지만, 실제 네트워크 요청은 한 번만 발생한다. 두 번째 호출부터는 메모리에 저장된 데이터를 재사용한다.

최상단에서 데이터를 받아와 props로 내려주지 않고, 매 컴포넌트에서 여러 번 fetch 요청을 보내도 처음 cache MISS 후 받아온 데이터를 메모리에 저장해두기에 그 뒤의 요청들은 cache HIT이 되어 추가적인 네트워크 요청 없이 메모리에 저장해둔 값을 사용하면 되는 것이다.

위 그림을 보면,
1. 첫 번째 fetch 요청은 cache MISS로 처리되며 데이터를 가져온다.
2. 이후 동일한 요청들은 메모리에 저장된 결과(cache HIT)을 사용

유의해야 할 점은, 렌더링 패스가 완료되면 메모리가 "리셋"되고 모든 요청 메모이제이션 항목이 지워진다는 것과, Request Memoization은 오직 fetch에서만 가능하다는 것이다. (다른 HTTP 클라이언트에서는 별도의 작업이 필요하다)


Data Cache

Data Cache는 fetch 함수를 기반으로 데이터를 캐싱하여, 서버 요청 간에도 데이터를 지속적으로 활용할 수 있게 해준다. 이는 외부 데이터 소스에 대한 요청 수를 줄이고, 응답 시간을 개선하는 데 큰 도움이 된다.

이 캐싱데이터는 배포한 후에도 지워지지 않기 때문에 적절한 세팅이 반드시 필요하다.

const res = await fetch('<https://api.example.com/data>', { next: { revalidate: 3600 } });
const data = await res.json();

위 코드의 {next: {revalidate: 3600}} 처럼 옵션을 걸어주어 일정 시간(3600초) 동안은 캐싱된 데이터를 사용하고, 일정 시간 후의 첫 요청 때 새로 데이터를 가져와 캐싱하게 할 수 있다.

그리고, 특정 데이터를 캐싱해두지 않고 매번 최신 데이터를 가져오고 싶은 경우에는 다음과 같이 옵션을 설정할 수 있다.

const res = await fetch('<https://api.example.com/data>', { cache: 'no-store' });
const data = await res.json();

{cache: 'no-store'} 옵션을 사용하면 해당 요청은 캐싱되지 않아 매번 새로운 데이터를 가져오게 된다.
(참고: default(아무것도 입력하지 않을 때)는 cache: 'force-cache'로 항상 캐싱됨)

위 이미지를 보면 force-cache의 경우 처음 Data Cache MISS 후 SET을 해두어 그 뒤부터는 캐싱된 값을 반환하게 되지만, no-store는 Data Cache 자체를 SKIP 하고 매 요청마다 Data Source에 직접 요청을 보내는 것을 알 수 있다.

Data Cache와 Request Memoization의 차이점

Request Memoization은 렌더링 서버에서 데이터 캐시 서버(예: CDN 또는 Edge Network)나 데이터 소스(예: 데이터베이스 또는 CMS)로의 네트워크 경계를 넘어야 하는 중복 요청 수를 줄임 + 렌더링 패스 완료 -> 리셋

Data Cache는 통해 원본 데이터 소스로의 요청 수를 줄임 + 특정 옵션이 없으면 리셋 안됨


Router Cache

지금까지는 모두 서버에서 발생하는 캐싱이였다면 Router Cache는 클라이언트 측에서 발생하는 캐싱이다.

이미 방문한 route는 MISS 후 캐싱해두어 다음에 해당 route를 재방문할 때는 캐싱된 데이터를 받게 되는 걸 알 수 있다.
추가적으로 방문하지 않은 route여도 화면에 <Link> 컴포넌트가 뷰포트에 나타나는 순간 해당 route를 prefetch하여, 클릭 시 즉시 페이지를 로드할 수 있다.


캐싱 방식 요약

캐싱 방식적용 대상주요 특징적합한 사례
Full Route Cache페이지 전체 (HTML)빌드 시 HTML 캐싱, 정적인 콘텐츠에 적합블로그, 문서 페이지
Request Memoization서버 렌더링 중 fetch 요청중복 요청 방지, 렌더링 종료 시 메모리 리셋공통 데이터 요청
Data Cache데이터(fetch 결과)데이터 소스 요청 수 줄임, 주기적 갱신 가능사용자 대시보드, 실시간 데이터 페이지
Router Cache클라이언트 경로방문한 route 캐싱 및 prefetch네비게이션 최적화

0개의 댓글