일반적으로 웹 애플리케이션을 개발할 때 Web API에서 제공하는 fetch를 사용하게 됩니다. Next.js는 이러한 네이티브 fetch Web API를 확장하여 서버에서 각 fetch 요청의 캐싱 및 재검증 동작을 구성할 수 있게 해줍니다. Next.js에서는 서버 컴포넌트, 라우트 핸들러, 서버 액션에서 async/await와 함께 fetch를 사용할 수 있습니다.
데이터 fetching 예시
async function getPosts() { // 서버 컴포넌트에서 데이터를 받아오는 함수 const response = await fetch('https://api.example.com/posts'); if (!response.ok) { throw new Error('Failed to fetch posts'); } return response.json();}async function Page() { // 페이지 컴포넌트 const posts = await getPosts(); return ( <main> {posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} </main> );}export default Page;
캐싱과 재검증
일반적으로 캐싱(cache)은 자주 사용되는 데이터를 캐시에 저장해놓고 다시 사용함으로써 불필요한 연산을 줄이는 방법을 의미합니다.
데이터 fetching과 캐싱
동일한 요청이 왔을 때 다시 서버에 요청하지 않고 캐시에 저장된 데이터를 사용하는 것을 의미
Next.js에서는 기본적으로 데이터 캐시(data cache)를 캐싱하지 않음
캐시를 강제로 사용하도록 하고 싶다면 cache 옵션의 값을 force-cache로 설정
재검증
캐싱은 캐싱 전략에 따라서 작동하는데, 캐싱과 항상 같이 나오는 개념이 바로 재검증(revalidation)입니다.
재검증은 캐시된 데이터를 최신 상태로 유지하기 위해 서버에서 새로운 데이터를 받아와 기존 데이터를 갱신하는 과정
재검증이 필요한 이유는 캐싱된 데이터가 항상 최신 데이터가 아니기 때문
캐싱된 시점부터 시간이 조금이라도 지난 이후에는 오래된 데이터로 간주되는 것
데이터 재검증을 하지 않고 계속해서 캐싱된 데이터를 사용하게 되면 사용자에게는 잘못된 데이터를 보여주는 결과를 초래
이렇게 시간이 지난 데이터를 스테일(stale) 데이터라고 함
시간 기반 재검증
시간 기반 재검증(time-based revalidation)은 이름이 가진 의미대로 시간을 기반으로 재검증하는 방식입니다.
데이터가 캐싱되고 일정 시간이 지난 이후에 데이터를 자동으로 재검증하는 것, 데이터가 자주 변경되지 않고 항상 최신 데이터가 필요한 것이 아닐 경우에 주로 사용
fetch의 next.revalidate 옵션에 시간 간격 값을 넣음으로써 캐싱된 데이터의 수명(초단위)을 설정할 수 있음
// 최대 1시간 이후 재검증fetch('https://api.example.com/posts', { next: { revalidate: 3600} });
만약 정적으로 렌더링된 라우트에서 여러 fetch 요청이 있고 각각이 다른 재검증 주기를 가지고 있는 경우, 가장 짧은 시간이 모든 요청에 적용
동적으로 렌더링된 라우트에서는 각 fetch 요청이 독립적으로 재검증
// 값으로 false, , number 사용 가능export const revalidate 3600; // 3600초(1시간)마다 재검증
온디맨드 재검증
온디맨드 재검증(on-demand revalidation)은 필요한 경우에 직접 재검증을 요청하는 방식입니다. 태그 또는 경로를 기반으로 한 번에 데이터 그룹 전체를 재검증하게 됩니다.
당장 최신 데이터를 보여줘야 할 필요가 있을 경우에는 온디맨드 재검증을 사용하는 것을 권장
온디맨드 재검증을 사용하려면 서버 액션 또는 라우트 핸들러 내에서 revalidatePath 함수를 사용하여 특정 경로에 대해 재검증
Next.js는 라우트 전체에서 fetch 요청을 무효화하기 위한 캐시 태깅 시스템(cache tagging system)을 갖추고 있음
fetch를 사용할 때 하나 이상의 태그로 캐시 항목에 태그를 할당할 수 있으며, next/cache 패키지의 revalidateTag 함수를 호출하여 해당 태그와 관련된 모든 항목을 재검증할 수 있음
async function Page() { // fetch 요청에 posts라는 캐시 태그를 추가하는 코드 const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] } }); const data = await res.json(); return ( // 예시: 받아온 데이터를 화면에 렌더링 <main> {data.map((post) => ( <div key={post.id}>{post.title}</div> ))} </main> );}export default Page;
서버 액션에서 posts라는 캐시 태그에 대해 revalidateTag 함수를 호출함으로써 posts 태그가 지정된 fetch 호출을 모두 재검증할 수 있음
'use server'; import { revalidateTag } from 'next/cache';export default async function action() { await revalidateTag('posts');}
서버 액션
서버 액션(server actions)은 리액트 19에서 정식 출시된 기능으로, 클라이언트 컴포넌트가 서버에서 실행되는 비동기 함수를 호출할 수 있게 해주는 기능입니다. 쉽게 말하면 클라이언트 컴포넌트에서 서버 측 데이터를 가져오거나 데이터베이스에 쿼리를 보낼 수 있게 해주는 것입니다.
서버 액션을 사용하기 위해서는 use server 지시어를 사용
이 지시어를 비동기 함수의 맨 위에 작성하여 해당 함수를 서버 액션으로 정의할 수 있음
파일의 맨 위에 작성하여 해당 파일의 모든 익스포트를 서버 액션으로 정의할 수 있음
Next.js에서는 서버 컴포넌트와 클라이언트 컴포넌트에서 서버 액션을 사용하여 폼(form)을 제출하거나 데이터 변경을 처리하는 데 사용
‘use server’ 지시어와 함께 서버 액션이 정의되면 Next.js는 자동으로 해당 서버 함수에 대한 참조를 생성하고, 그 참조를 클라이언트 컴포넌트에 전달합니다. 그리고 클라이언트에서 해당 함수가 호출되면 리액트는 서버에 요청을 보내서 그 함수를 실행하고 결과를 반환하게 됩니다.
서버 액션은 서버 컴포넌트에서 생성하여 클라이언트 컴포넌트의 props로 전달할 수도 있고, 클라이언트 컴포넌트에서 임포트하여 사용할 수도 있음
서버 컴포넌트에서는 인라인 함수 레벨 또는 모듈 레벨에서 ‘use server’ 지시어를 사용하여 서버 액션을 정의할 수 있음
서버 액션을 정의하면 리액트가 WritePost라는 서버 컴포넌트를 렌더링할 때 createPostAction() 함수의 레퍼런스를 생성하고 그것을 Button이라는 클라이언트 컴포넌트로 전달하게 됨
import Button from './Button';function WritePost() { // Server Actions 정의 async function createPostAction() { 'use server'; await db.posts.create(); } return <Button onClick={createPostAction} />;}export default WritePost;
Button 컴포넌트의 코드를 나타낸 것인데, 여기서 onClick을 콘솔 로그로 출력해보면 레퍼런스가 들어 있는 것을 볼 수 있음
이렇게 하게 되면 번들러가 WritePost 클라이언트 컴포넌트를 빌드할 때 번들 안에 createPostAction() 함수에 대한 레퍼런스를 생성합니다. 그리고 버튼이 클릭되면 리액트는 제공 된 레퍼런스를 사용하여 createPostAction() 함수를 실행하기 위해 서버에 요청을 보내게됩니다. 그리고 추가로 다음과 같이 서버 액션을 클라이언트 컴포넌트에 props로 전달해서 사용할 수 있습니다.
'use client';function ClientComponent({ myAction }) { return ( <form action={myAction}> {/* 폼 요소 및 입력 필드 추가 */} </form> );}export default ClientComponent;