page.js에서 데이터 페칭(Fetching)과 캐싱 전략

현대 웹 개발에서 '속도'는 곧 '사용자 경험'과 직결됩니다. 과거에는 서버에서 모든 데이터를 가져와 뿌려주거나(SSR), 브라우저가 텅 빈 페이지를 받은 뒤 자바스크립트로 데이터를 채우는(CSR) 양극단의 방식이 주를 이뤘습니다.
하지만 Next.js의 App Router 환경은 이 둘의 장점을 영리하게 결합했습니다. 특히 page.js에서 데이터를 어떻게 가져오고(Fetching), 어떻게 보관하느냐(Caching)에 따라 서비스의 비용과 성능이 결정됩니다. 오늘은 실무에서 가장 고민되는 Next.js 데이터 전략을 심도 있게 파헤쳐 보겠습니다.
1. 데이터 페칭의 심장: 왜 서버 컴포넌트인가?
Next.js App Router의 page.js는 기본적으로 **서버 컴포넌트(Server Components)**입니다. 여기서 데이터를 가져오면 다음과 같은 강력한 이점이 있습니다.
- 백엔드와의 거리 단축: 데이터베이스나 API 서버와 물리적으로 가까운 곳에서 통신하므로 지연 시간(Latency)이 줄어듭니다.
- 보안: API 키나 민감한 로직이 브라우저로 노출되지 않습니다.
- 번들 사이즈 감소: 데이터를 처리하기 위한 무거운 라이브러리를 클라이언트에 보낼 필요가 없습니다.
💡 비유로 이해하기: 뷔페 vs 주문 요리
- 기존 방식(CSR): 손님이 빈 접시를 들고 주방에 가서 재료를 하나하나 주문하고 기다리는 것과 같습니다.
- 서버 페칭: 주방장(서버)이 미리 요리를 완성해서 손님 테이블(page.js)에 바로 내놓는 방식입니다. 손님은 앉자마자 식사를 시작할 수 있죠.
2. 실전 예제: 이커머스 상품 상세 페이지 구현
단순한 "Hello World" 대신, 실제 서비스에서 발생할 법한 상품 상세 정보와 실시간 재고를 결합한 시나리오를 코드로 구현해 보겠습니다.
// app/products/[id]/page.js
import { notFound } from 'next/navigation';
/**
* 상품 정보를 가져오는 함수
* next.revalidate 옵션을 통해 캐싱 전략을 제어합니다.
*/
async function getProduct(id) {
// 1. fetch API의 확장된 기능을 사용
const res = await fetch(`https://api.example.com/products/${id}`, {
// 3600초(1시간) 동안 캐시를 유지하고, 이후에는 백그라운드에서 갱신합니다. (ISR)
next: { revalidate: 3600 }
});
if (!res.ok) {
if (res.status === 404) return null;
throw new Error('데이터를 불러오는 데 실패했습니다.');
}
return res.json();
}
export default async function ProductPage({ params }) {
const { id } = params;
const product = await getProduct(id);
// 데이터가 없을 경우 Next.js의 내장 404 페이지로 유도
if (!product) {
notFound();
}
return (
<main className="p-8">
<article className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="mt-4 text-gray-600">{product.description}</p>
<div className="mt-8 p-4 border rounded-lg bg-gray-50">
<h2 className="text-xl font-semibold mb-2">구매 정보</h2>
{/* 실시간성이 중요한 재고 데이터는 별도 처리가 필요할 수 있습니다 */}
<p>가격: {product.price.toLocaleString()}원</p>
<p className={product.stock > 0 ? 'text-blue-600' : 'text-red-600'}>
{product.stock > 0 ? `현재 ${product.stock}개 남음` : '품절'}
</p>
</div>
</article>
</main>
);
}
🔍 핵심 로직 분석
- Async/Await: page.js 자체가 async 함수가 되어 서버에서 비동기 데이터를 직접 기다립니다.
- Extended Fetch: Next.js는 브라우저의 fetch를 확장하여 서버 측에서도 **캐싱(Caching)**과 **재검증(Revalidation)**을 지원합니다.
- Error Handling: notFound() 함수를 호출해 예외 상황을 깔끔하게 처리합니다.
3. 캐싱 전략: 데이터의 '유통기한' 설정하기
데이터의 성격에 따라 우리는 세 가지 선택지를 가집니다.
| 전략 | 설명 | 활용 사례 |
| Force Cache | 영구적으로 캐싱합니다. (기본값) | 공지사항, 회사 소개 |
| Revalidate (ISR) | 설정한 시간마다 데이터를 갱신합니다. | 상품 목록, 블로그 포스트 |
| No Store | 캐싱하지 않고 매 요청마다 새로 가져옵니다. | 개인 장바구니, 실시간 주식 시세 |
트러블슈팅 팁: "분명 코드를 수정했는데 데이터가 안 바뀌어요!"
이것은 Next.js의 강력한 캐싱 때문입니다. 개발 단계에서 강제로 최신 데이터를 보고 싶다면 fetch 옵션에 { cache: 'no-store' }를 추가하거나, 브라우저 주소창에 주소를 다시 치고 엔터를 눌러보세요. (단순 새로고침은 캐시를 탈 수 있습니다.)
4. Trade-offs: 무엇을 포기하고 무엇을 얻는가?
모든 기술에는 대가가 따릅니다. 서버 페칭도 마찬가지입니다.
- 장점: 첫 화면 로딩 속도(FCP)가 압도적으로 빠르며, SEO에 매우 유리합니다.
- 한계: page.js에서 너무 많은 데이터를 await 하면, 모든 데이터가 준비될 때까지 사용자는 흰 화면을 보게 됩니다.
- 대안: 이럴 때는 **Suspense**와 **loading.js**를 활용하세요. 중요도가 낮은 하위 컴포넌트는 클라이언트에서 페칭하거나 스트리밍 방식으로 렌더링하는 것이 현명합니다.
결론: 당신의 데이터는 얼마나 '신선'해야 하나요?
Next.js page.js에서의 데이터 페칭은 단순히 값을 가져오는 행위를 넘어, 사용자에게 어떤 속도로 어떤 품질의 정보를 전달할 것인가를 결정하는 전략적인 선택입니다.
모든 데이터를 실시간으로 가져올 필요는 없습니다. 1분 전의 뉴스나 1시간 전의 상품 가격은 대개 사용자에게 큰 불편을 주지 않습니다. 서비스의 성격에 맞춰 캐싱 주기를 설정해 보세요. 불필요한 서버 부하를 줄이면서도 사용자에게는 빛처럼 빠른 응답 속도를 선사할 수 있습니다.