본문 바로가기
(Frontend) 프론트엔드

(Next.js + App Router) Next.js에서 fetch 사용해야 하는 이유 - 캐싱 메커니즘과 14 vs 15 버전 차이

by 공부가싫다가도좋아 2026. 6. 27.
반응형

포스팅 목차

  1. 이 포스팅에서 다루는 것
  2. Next.js에서 fetch를 권장하는 이유 - axios VS fetch
  3. Request Memoization - 같은 요청은 한 번만 실행하기
  4. Data Cache - fetch 캐싱 동작 원리
  5. Next.js 14 vs 15 - 달라진 캐싱 기본값
  6. cache 옵션 완전 정리 - force-cache / no-store / revalidate 비교
  7. 공부하면서 생겼던 의문점 (Q&A)
  8. 참고 자료

1. 이 포스팅에서 다루는 것

Next.js App Router를 처음 접하면 공식 문서에서 좀 의아한 부분을 만납니다.

axios 같은 익숙한 라이브러리를 두고 네이티브 fetch를 직접 쓰라고 권장하기 때문입니다. 여기에 더해, 14에서 15로 버전을 올리면 캐싱 동작까지 달라져서 혼란을 겪는 경우도 많습니다.

이 글에서는 Next.js가 fetch를 확장한 방식과 그 이유, 14버전과 15버전에서 캐싱 기본값이 어떻게 달라졌는지를 개념부터 정리해보겠습니다.

- Next.js App Router 기준이며, 캐싱 기본값은 최신 15 버전을 기준으로 설명합니다.
- 14버전과 15버전의 캐싱 기본값 차이를 다룹니다.
- 실무 코드 예시는 2탄(실무편)에서 이어집니다.


2. Next.js에서 fetch를 권장하는 이유 - axios VS fetch

2-1. Next.js는 네이티브 fetch를 확장했습니다

Next.js는 브라우저 표준 Web API인 fetch를 그대로 사용하되, 서버 사이드에서 캐싱과 재검증(revalidation) 기능을 추가로 확장했습니다.

브라우저에서 cache 옵션은 브라우저 HTTP 캐시와 상호작용하는 방식을 지정합니다. Next.js에서는 이 옵션이 프레임워크 레벨의 서버 사이드 캐시와 상호작용하는 방식을 지정합니다. 같은 API지만 동작하는 레이어가 다릅니다.

// 브라우저 fetch → 브라우저 http 캐시와 상호작용
fetch('/api/data', { cache: 'no-store' });

// next.js 서버에서의 fetch → next.js 프레임워크 캐시와 상호작용
// + next 옵션으로 next.js 전용 기능 사용 가능
fetch('https://api.example.com/data', {
  cache: 'force-cache',
  next: { revalidate: 3600, tags: ['products'] },
});

 

2-2. axios 대신 fetch를 써야 하는 이유

axios는 좋은 라이브러리지만, Next.js App Router에서는 fetch 확장 기능의 혜택을 받을 수 없습니다.

axios로 응답을 캐싱하려면 React Query 같은 라이브러리를 따로 써야 하는데, 이런 라이브러리는 클라이언트 컴포넌트('use client')에서 써야 합니다. 그러면 데이터 패칭이 클라이언트로 넘어가서, Next.js의 강점인 React Server Component(RSC)를 활용하기 어려워집니다.

구분 axios Next.js fetch
Request Memoization 지원 안 됨 자동 적용
Data Cache (서버 사이드) 지원 안 됨 지원
revalidate 옵션 없음 있음 (next.revalidate)
on-demand revalidation (태그) 없음 있음 (next.tags)
추가 설치 필요 필요 불필요 (내장)
Server Component 최적화 해당 없음 최적화됨

axios는 클라이언트나 별도 백엔드 서버에서 쓰기엔 여전히 좋은 선택입니다. 다만 Next.js Server Component에서 외부 API를 호출할 때는 fetch를 써야 Next.js가 제공하는 캐싱 최적화를 온전히 활용할 수 있습니다.

클라이언트 컴포넌트에서는?
클라이언트 컴포넌트에서는 React Query나 SWR 같은 라이브러리가 데이터 패칭을 맡습니다. Next.js fetch 확장 기능(캐시, revalidate 등)은 서버에서 실행될 때만 동작합니다.


3. Request Memoization - 같은 요청은 한 번만 실행하기

3-1. Memoization이란?

한 페이지를 렌더링하는 동안 여러 Server Component에서 같은 API를 호출하는 일이 생깁니다. 예를 들어 Layout, Page, 여러 하위 컴포넌트에서 각각 사용자 정보를 가져오면 같은 요청이 여러 번 나갈 수 있습니다.

Next.js는 같은 URL과 옵션으로 호출된 GET fetch 요청을 하나의 서버 렌더링 사이클 내에서 자동으로 중복 제거합니다. 이를 Request Memoization이라고 합니다.

// layout 컴포넌트
async function layout() {
  const user = await fetch('https://api.example.com/me'); // 1번 실행
  // ...
}

// page 컴포넌트
async function page() {
  const user = await fetch('https://api.example.com/me'); // 중복 → 실제 요청 안 함, 캐시 반환
  // ...
}

// 하위 컴포넌트
async function userinfo() {
  const user = await fetch('https://api.example.com/me'); // 중복 → 실제 요청 안 함, 캐시 반환
  // ...
}

// 실제 네트워크 요청은 1번만 발생합니다.

 

3-2. Memoization의 범위

Memoization은 하나의 서버 렌더링 요청(request) 안에서만 유효합니다. 다음 사용자가 페이지를 요청하면 다시 초기화됩니다. 영구 캐시를 원한다면 Data Cache(4장)를 사용해야 합니다.

구분 Request Memoization Data Cache
유효 범위 단일 렌더링 사이클 서버 재시작 전까지 (영구)
초기화 시점 요청마다 초기화 revalidate 주기 또는 수동 초기화
설정 필요 여부 자동 적용 cache 옵션으로 명시
적용 대상 GET 요청만 GET 요청만

Route Handler에서는 Memoization이 적용되지 않습니다
Memoization은 React 컴포넌트 트리 안에서만 동작합니다. app/api/ Route Handler 내부의 fetch는 Memoization 대상이 아닙니다.


4. Data Cache - fetch 캐싱 동작 원리

4-1. Data Cache란?

Data Cache는 Next.js 서버에 존재하는 영구적인 서버 사이드 캐시입니다. Request Memoization이 단일 요청 안에서만 유효한 것과 달리, Data Cache는 서버가 재시작되거나 명시적으로 무효화할 때까지 캐시된 데이터를 유지합니다.

fetch 요청에 cache: 'force-cache'next.revalidate 옵션을 지정하면 Data Cache가 활성화됩니다.

// data cache에 저장 (영구 캐시)
const res = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
});

// data cache에 저장 (1시간마다 재검증)
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 },
});

// data cache 사용 안 함 (매 요청마다 새로 가져옴)
const res = await fetch('https://api.example.com/products', {
  cache: 'no-store',
});

 

4-2. Data Cache 동작 흐름

fetch 요청 발생
      │
      ▼
data cache에 캐시된 데이터가 있는가?
      │
      ├── 있음 (fresh) → 캐시 반환 (네트워크 요청 없음) (o)
      │
      ├── 있음 (stale, revalidate 기간 초과)
      │       └── 캐시 반환 + 백그라운드에서 재검증 후 캐시 갱신
      │
      └── 없음 → 외부 api 호출 → 응답을 캐시에 저장 후 반환

 

4-3. on-demand revalidation - 태그로 캐시 무효화

next.tags 옵션을 쓰면 특정 태그가 달린 캐시만 골라서 무효화할 수 있습니다. 예를 들어 상품 데이터를 업데이트했을 때 상품 관련 캐시만 즉시 비웁니다.

// 1. fetch 시 태그 지정
const res = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] },
});

// 2. 상품 업데이트 후 해당 태그 캐시만 무효화
// (server action 또는 route handler에서 호출)
import { revalidatetag } from 'next/cache';

export async function updateproduct() {
  await db.products.update({ ... });
  revalidatetag('products'); // 'products' 태그가 달린 캐시 전체 무효화
}

5. Next.js 14 vs 15 - 달라진 캐싱 기본값

5-1. 무엇이 달라졌나?

Next.js 14에서 15로 올린 뒤 페이지 로딩이 느려지거나, 코드를 그대로 뒀는데 동작이 달라지는 일을 겪기도 합니다. 원인은 fetch의 기본 캐시 동작이 바뀐 데 있습니다.

구분 Next.js 14 Next.js 15
fetch 기본 동작 force-cache (캐시 O) auto no-cache (캐시 X)
옵션 미지정 시 빌드 시 캐시되어 영구 저장 매 요청마다 새로 가져옴
캐시를 쓰려면 기본값 (아무것도 안 해도 됨) force-cache 또는 revalidate 명시
캐시를 끄려면 cache: 'no-store' 명시 기본값 (아무것도 안 해도 됨)

 

5-2. 코드로 보는 변화

// ─── next.js 14 ───────────────────────────────────────
async function getproducts() {
  // 옵션 없음 → 기본값이 force-cache → 빌드 시 캐시되어 영구 저장
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

// 캐시를 끄고 싶으면 명시해야 했음
const res = await fetch('https://api.example.com/products', {
  cache: 'no-store', // 명시해야 동적 데이터
});

// ─── next.js 15 ───────────────────────────────────────
async function getproducts() {
  // 옵션 없음 → 기본값이 auto no-cache → 매 요청마다 새로 가져옴
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

// 캐시를 쓰고 싶으면 명시해야 함
const res = await fetch('https://api.example.com/products', {
  cache: 'force-cache', // 명시해야 캐시됨
});

 

5-3. 왜 변경했나?

Next.js 14의 기본값이 force-cache였을 때, 캐싱이 되고 있다는 걸 모른 채 "왜 데이터가 안 바뀌지?" 하고 혼란을 겪는 일이 많았습니다.

Vercel 팀은 "암묵적인 캐싱보다 명시적인 캐싱이 낫다"는 판단으로 기본값을 바꿨습니다. 캐시가 필요하면 직접 명시하도록 유도한 셈입니다.

14에서 15로 업그레이드 시 주의사항
cache 옵션을 지정하지 않은 fetch가 있다면, 14버전에서는 캐시됐지만 15버전에서는 매 요청마다 새로 가져옵니다. 성능에 민감한 페이지라면 업그레이드 전에 fetch 호출부를 점검하고 필요한 곳에 cache: 'force-cache' 또는 next: { revalidate }를 명시해야 합니다.


6. cache 옵션 완전 정리 - force-cache / no-store / revalidate 비교

6-1. cache 옵션별 동작

옵션 동작 적합한 데이터
기본값 (v15)
auto no-cache
매 요청마다 새로 가져옴 (개발 환경). 빌드 시엔 정적 프리렌더링 일반적인 API 호출
cache: 'no-store' 항상 매 요청마다 새로 가져옴 (정적 프리렌더링도 하지 않음) 실시간 데이터, 사용자 개인화 데이터
cache: 'force-cache' 영구 캐시. 캐시 없으면 가져와서 저장. 만료 없음 변경이 거의 없는 정적 데이터
next: { revalidate: N } N초마다 백그라운드 재검증 (ISR 방식) 주기적으로 갱신되는 데이터
next: { revalidate: false } force-cache와 동일. 영구 캐시 변경이 거의 없는 정적 데이터
next: { revalidate: 0 } no-store와 동일. 캐시 안 함 실시간 데이터

 

6-2. 각 옵션 코드 예시

// 1. 영구 캐시 - 빌드 시 한 번 가져와서 계속 재사용
//    공지사항, 약관, 카테고리 목록 등 거의 바뀌지 않는 데이터
const res = await fetch('https://api.example.com/categories', {
  cache: 'force-cache',
});

// 2. 시간 기반 재검증 - 1시간마다 백그라운드에서 갱신
//    상품 목록, 블로그 포스팅 등 주기적으로 갱신되는 데이터
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 },
});

// 3. 캐시 없음 - 매 요청마다 새로 가져옴
//    사용자 프로필, 장바구니, 알림 등 사용자별 실시간 데이터
const res = await fetch('https://api.example.com/me/cart', {
  cache: 'no-store',
});

// 4. 태그 기반 on-demand revalidation - 이벤트 발생 시 즉시 갱신
//    관리자가 상품을 수정할 때 즉시 반영해야 하는 경우
const res = await fetch('https://api.example.com/products', {
  next: { tags: ['products'], revalidate: 3600 },
});

 

6-3. 충돌하는 옵션 조합은 사용할 수 없습니다

revalidatecache: 'no-store'는 서로 충돌하는 옵션입니다. 함께 사용하면 두 옵션 모두 무시되고 개발 환경에서는 경고가 출력됩니다.

// 잘못된 조합 - 두 옵션이 충돌하여 모두 무시됨
const res = await fetch('https://api.example.com/products', {
  cache: 'no-store',
  next: { revalidate: 3600 }, // 경고 발생, 무시됨
});

// 올바른 조합
// 캐시를 끄려면
const res1 = await fetch('https://api.example.com/products', {
  cache: 'no-store',
});

// 주기적 갱신을 원하면
const res2 = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 },
});

7. 공부하면서 생겼던 의문점 (Q&A)

Q. Server Component에서 axios를 절대 쓰면 안 되나요?

A. 기술적으로 쓸 수는 있습니다. 단, axios를 쓰면 Next.js의 Request Memoization, Data Cache, revalidate, tags 같은 캐싱 최적화를 전혀 활용하지 못합니다. 외부 API 호출이 잦은 서버 컴포넌트라면 fetch를 쓰는 편이 낫습니다.

Q. 개발 환경에서 캐시가 제대로 동작하지 않는 것 같습니다.

A. 의도된 동작입니다. Next.js는 개발 환경에서 HMR(Hot Module Replacement) 중에도 fetch 응답을 별도로 캐시합니다. 단, 이 캐시는 페이지 네비게이션이나 전체 새로고침 시에 초기화됩니다. 캐싱 동작을 정확히 확인하려면 next build && next start로 프로덕션 모드에서 테스트해야 합니다.

Q. Memoization을 비활성화할 수 있나요?

A. AbortControllersignal을 fetch에 전달하면 Memoization에서 제외됩니다. 단, 같은 데이터를 여러 컴포넌트에서 가져오는 일반적인 케이스에서는 비활성화할 이유가 거의 없습니다.

Q. revalidate 값이 여러 fetch에서 다르면 어떻게 되나요?

A. 같은 라우트 안에 revalidate 값이 다른 fetch가 여럿 있으면, 가장 낮은 값이 라우트 전체에 적용됩니다. 예를 들어 한 페이지에서 revalidate: 60revalidate: 3600을 함께 쓰면, 전체 라우트가 60초마다 재검증됩니다.

Q. React Query 캐싱과 fetch 캐싱은 무엇이 다른가요?

A. 캐시가 사는 위치와 공유 범위가 다릅니다. Next.js fetch의 Data Cache는 서버에 저장되어 모든 사용자가 공유하는 서버 사이드 캐시입니다. 예를 들어 누구에게나 똑같이 보이는 상품 목록을 next: { revalidate: 3600 }로 가져오면, 서버가 한 번 캐싱해 둔 응답을 모든 방문자가 함께 받습니다. 그래서 초기 렌더링이나 SEO용 데이터에 잘 맞습니다.

반면 React Query 캐시는 브라우저 메모리에 저장되는 클라이언트 캐시라서 사용자마다 따로 존재합니다. 예를 들어 사용자가 입력한 검색어 결과나 무한 스크롤로 더 불러온 목록을 그 사람 브라우저에만 캐싱해 두고, 같은 조건으로 다시 조회하면 네트워크 요청 없이 바로 보여줍니다.

다만 서버에서 공유된다는 점이 오히려 문제가 되기도 합니다. 사용자마다 달라야 하는 데이터, 예를 들어 장바구니나 내 프로필을 cache: 'force-cache'로 캐싱하면, 먼저 캐시에 올라간 A 사용자의 데이터가 뒤이어 접속한 B 사용자에게 그대로 보일 수 있습니다. 그래서 개인화 데이터는 반드시 cache: 'no-store'로 매 요청마다 새로 가져와야 합니다.

Q. Next.js에서는 별도 설정 없이 fetch가 자동으로 확장되나요?

A. 네. Next.js 안에서 fetch를 호출하면 따로 설정하지 않아도 Next.js가 확장한 fetch가 쓰입니다. Request Memoization은 옵션을 주지 않아도 자동으로 적용되고, cachenext 옵션은 Data Cache와 revalidate를 켜고 끌 때만 직접 지정하면 됩니다. 정리하면 "확장된 fetch를 쓰는 것" 자체는 자동이고, "어떻게 캐싱할지"만 옵션으로 고르는 구조입니다.

Q. 그 자동 확장은 어떻게 가능한가요?

A. Next.js가 전역 fetch 함수를 자체 구현으로 감싸 두기 때문입니다. 서버에서 코드가 실행될 때는 브라우저 표준 fetch가 아니라 Next.js가 덮어쓴 fetch가 호출되고, 그 안에서 Memoization, Data Cache, revalidate 같은 기능이 처리됩니다. 그래서 평소처럼 fetch를 부르기만 해도 추가 설치나 import 없이 확장 기능이 따라옵니다. 다만 이 확장은 서버에서 실행될 때만 동작하고, 클라이언트 컴포넌트에서는 표준 fetch 그대로 동작합니다.


8. 마무리

Next.js에서 fetch를 그저 "HTTP 요청 도구"로만 보면 왜 axios 대신 굳이 fetch를 써야 하는지 납득하기 어렵습니다. 하지만 Request Memoization과 Data Cache, revalidate까지 이해하고 나면 fetch가 Next.js 렌더링 전략과 긴밀하게 맞물린 인터페이스라는 게 보입니다.

핵심을 정리하면 다음과 같습니다.

Next.js는 fetch를 확장해 서버 사이드 캐싱을 제어한다 → Memoization은 단일 렌더링 내 중복 요청을 제거하고, Data Cache는 서버 레벨에서 영구 캐시를 관리한다 → 14버전은 기본값이 캐시 ON, 15버전은 기본값이 캐시 OFF → 옵션은 항상 명시적으로 지정하는 것이 안전하다

2탄에서는 실제 Server Component에서 fetch를 어떻게 패턴화해 쓰는지, React Query와 역할을 어떻게 나누는지를 코드 위주로 정리해보겠습니다.


참고 자료

반응형

댓글