[Next.js] 4. Data Fetching, Caching, ISR
1. Data Fetching
백엔드 API나 데이터베이스 등 외부 소스에서 데이터를 가져오는 과정을 말한다.
가져온 데이터를 기반으로 페이지나 컴포넌트를 렌더링한다.
Data Fetching은 렌더링 위치에 따라 둘로 나눌 수 있다.
하나는 서버이고, 다른 하나는 클라이언트이다.
2. 서버에서 Data Fetching
+ 다이나믹 API 사용이 없는 라우트는 프리랜더링된다. (static page의 형태로 프리랜더링)
(다이나믹 API : cookies, headers, searchParams...)
1. fetch API
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
2. ORM or DB
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
3. 클라이언트에서 Data Fetching
1. useEffect
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://api.vercel.app/blog')
const data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
if (!posts) return <div>Loading...</div>
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
2. React Libraries (SWR, React Query)
4. Caching
Next.js는 서버에서 데이터를 요청(fetch)할 때, 데이터를 메모리에 저장해두는 Data Cache 공간을 제공한다.
Next.js의 Data Cache는 서버 컴포넌트에서만 적용된다. Data Cache는 영구적으로 저장된다.
캐싱을 잘 이용하면 (결과가 같은) 과거 요청했던 것을 반복적으로 요청하는 것을 막아준다.
최신버전(version15+)에서는 수동 캐싱이 디폴트이다. (no cached by default.)
즉, { cache : "no-store" } 이 default 값이다.
따라서 { cache : "force-cache" } 를 추가해 줘야 한다.
// lib/getPosts.ts
export async function getPosts() {
const res = await fetch('https://~', {
cache: 'force-cache',
next: {
revalidate: 60, // 60초마다 ISR 방식으로 캐시 갱신
},
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
// app/page.tsx
import { getPosts } from '@/lib/getPosts';
export default async function HomePage() {
const posts = await getPosts();
return (
<main>
<h1>Posts</h1>
<ul>
{posts.slice(0, 5).map((post: any) => (
<li key={post.id}>
<strong>{post.title}</strong>
</li>
))}
</ul>
</main>
);
}
5. Revalidating Data (ISR)
캐싱은 불필요한 요청을 막아주지만 결국 데이터는 변할 수 있다.
불필요했던 요청이 필요해 질 수 있는 것이다.
따라서 자연스럽게 캐싱해둔 데이터를 버리고 최신상태의 데이터로 업데이트 하는 과정이 필요하다.
여기서 중요한 것은 빈도이다.
최신의 상태가 중요하거나 업데이트가 자주 일어나는 경우에는 캐싱없이 매번 요청을 진행하는 것이 낫고(SSR).
업데이트가 거의 일어나지 않는 경우에는 업데이트 과정을 고려할 필요가 없다(SSG).
이 두 경우의 사이에 존재하는 빈도의 변화가 있는 페이지가 ISR을 적용하기에 적절하다.
ISR은 Incremental Static Regeneration이다.
초기에 정적 생성된 페이지를(Static Generation) 빠르게 제공하지만 빌드 이후 데이터가 바뀌면
백그라운드에서 해당 페이지만 개별적으로 점진적으로(Incremental) 새로 만들어(Regeneration) 업데이트 하는 방식을 의미한다.
ISR의 적용을 통해서 정적 페이지의 속도와 서버 렌더링의 유연성을 동시에 가질 수 있다.
Revalidating 방법 (Data Cache 무효화)
1. 시간에 기반해서(Time-based revalidation)
일정 시간이 지나면 자동 재검증 - 변경빈도와 최신성 중요도가 낮은 경우
// lib/getPosts.ts
export async function getPosts() {
const res = await fetch('https://~', {
next: {
revalidate: 60, // 60초마다 ISR로 갱신
},
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
2. 이벤트에 기반해서(On-demand revalidation)
태그기반(revaliateTag), 경로기반(revaliatePath) 접근 방식을 이용한 수동 재검증 - 최신성 중요도가 높은 경우
// fetch에 태그 달기
await fetch('https://api.example.com/posts', {
next: {
revalidate: 0,
tags: ['posts'], // 태그 지정
},
});
// app/api/revalidate/posts/route.ts
import { revalidateTag } from 'next/cache';
export async function POST() {
revalidateTag('posts'); // 'posts' 태그 캐시 무효화
return Response.json({ revalidated: true });
}
// app/api/revalidate/page/route.ts
import { revalidatePath } from 'next/cache';
export async function POST() {
revalidatePath('/posts/1'); // 해당 경로 HTML 재생성
return Response.json({ revalidated: true });
}
종합해서 정리해보면~
렌더링의 시점과 위치를 모두 고려하면 Next.js에는 총 다섯가지의 데이터 패칭 전략이 존재한다.
전략 | 설명 | 실행 시기 | 특징 | 사용처 | 예시 |
Static Generation (SSG) | 빌드 시 데이터를 가져와 HTML 생성 (미리) | 빌드 시 | 정적 HTML 생성, 빠른 응답 속도, 변경 시 재배포 필요 | 정적 콘텐츠(블로그, 문서 등) | getStaticProps 또는 fetch() + force-cache |
Server-Side Rendering (SSR) | 매 요청마다 서버에서 데이터를 가져옴 (그때그때) | 요청마다 | 항상 최신 데이터 제공, 성능 비용 있음 | 자주 바뀌는 콘텐츠(상품 가격, 실시간 데이터 등) | getServerSideProps 또는 fetch() + no-store |
Incremental Static Regeneration (ISR) | 빌드 후에도 일정 주기로 데이터 갱신 | 요청 + 백그라운드 재생 | 정적 생성 + 일정 주기로 자동 갱신 | 중간 정도 빈도로 바뀌는 콘텐츠 | revalidate : N |
Client-Side Fetching | 브라우저에서 클라이언트 측에서 fetch | 페이지 로드 후 실행 | 사용자 브라우저에서 fetch | 자주 바뀌는 콘텐츠(상품 가격, 실시간 데이터 등) | useEffect 또는 SWR, React Query... |
React Server Actions (v14+) | 폼 제출 등 서버 액션에서 데이터 가져오기 | 서버에서만 실행 | 서버에서 안전하게 동작 | 사용자 상호작용 기반 패치(폼, 클릭 등) | async function action(formData) {} |
[Next.js] 5. Server Action, Form
1. 서버액션서버액션은 서버에서 수행되는 비동기 함수이다.서버액션은 form 상태관리, 캐싱, 유효성검사 관련하여 next에서 새롭게 제시하는 솔루션이다. form 데이터로 서버의 데이터 변경하는
samdasoo2l.tistory.com
(5, 6, 7은 추가적인 부분)
5. Suspense
loading.tsx를 써도 되지만
페이지 전체가 아닌 부분 별로 로딩 상태를 사용자에게 제공이 가능하다.
(show an instant loading state while React Streams in the result.)
import { Suspense } from 'react';
import Posts from '@/components/Posts';
export default function Page() {
return (
<main>
<h1>게시글</h1>
<Suspense fallback={<p>로딩 중...</p>}>
<Posts />
</Suspense>
</main>
);
}
6. Promise.all
기본적으로 같은 세그먼트 또는 컴포넌트 내부에 awaited request는 어떤 아래의 요청이든 차단한다.
즉, 데이터 패칭이 순차적으로 (차례차례) 진행된다. 당연히 시간이 오래 걸린다.
그래서 Promise.all을 통해서 데이터를 병렬적으로 패치할 필요가 있다.
(병렬적으로 모두 완료될 때까지 기다려야한다.)
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
const artistData = getArtist(username) // fetch 시작 (백그라운드에서 비동기 실행됨)
const albumsData = getAlbums(username) // 안 기다리고 또 다른 fetch 시작 (병렬로 동시에 시작)
// Initiate both requests in parallel
const [artist, albums] = await Promise.all([artistData, albumsData]) // 둘 다 완료 될 때까지 기다림
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
7. Prefetch
아직 사용자가 요청하지 않은 데이터나 페이지를 미리 불러올 수 있다.
즉, 선제적 요청을 의미한다.
Link 컴포넌트를 사용하면 자동으로 prefetch한다. (Link가 Viewport에 들어오면 prefetch)
(+ Link의 prefetch 속성을 false로 수정하면 prefetch를 끌 수 있다.)