웹 프로그래밍

데이터 페칭(data fetching)

2025-12-08

데이터 fetching

일반적으로 웹 애플리케이션을 개발할 때 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을 콘솔 로그로 출력해보면 레퍼런스가 들어 있는 것을 볼 수 있음
'use client';

function Button({ onClick }) {
  console.log(onClick);
  // ($$typeof: Symbol.for("react.server.reference"), $$id: 'createPostAction')
  return (
    <button onClick={onClick}>
      게시글 작성
    </button>
  );
}

export default Button;
  • 이 상태에서 사용자가 버튼을 클릭하면 리액트는 createPostAction() 함수의 레퍼런스와 함께 해당 함수를 실행하도록 서버에 요청
  • 이러한 흐름이 서버 컴포넌트에서 서버 액션이 작동하는 흐름이라고 보면 됨

클라이언트 컴포넌트에서 서버 액션 사용하기 그렇다면 클라이언트 컴포넌트에서 서버 액션을 사용하려면 어떻게 해야 할까요?

  • 클라이언트 컴포넌트에서는 ‘use server’ 지시어를 사용하는 파일로부터 서버 액션을 가져올 수 있음
  • 먼저 다음 코드와 같이 ‘use server’ 지시어를 사용하고 서버 액션이 정의된 별도의 파일을 작성함
// actions.ts
'use server';

export async function createPostAction() {
  await db.posts.create();
}
  • 이후 아래 코드와 같이 정의한 서버 액션을 클라이언트 컴포넌트에서 임포트해서 사용하면 됨
'use client';

import { createPostAction } from './actions';

function WritePost() {
  console.log(createPostAction);
  // {$$typeof: Symbol.for("react.server.reference"), $$id: 'createPostAction'}
  return (
    <button onClick={createPostAction}>
      게시글 작성
    </button>
  );
}

export default WritePost;

이렇게 하게 되면 번들러가 WritePost 클라이언트 컴포넌트를 빌드할 때 번들 안에 createPostAction() 함수에 대한 레퍼런스를 생성합니다. 그리고 버튼이 클릭되면 리액트는 제공 된 레퍼런스를 사용하여 createPostAction() 함수를 실행하기 위해 서버에 요청을 보내게됩니다. 그리고 추가로 다음과 같이 서버 액션을 클라이언트 컴포넌트에 props로 전달해서 사용할 수 있습니다.

'use client';

function ClientComponent({ myAction }) {
  return (
    <form action={myAction}>
      {/* 폼 요소 및 입력 필드 추가 */}
    </form>
  );
}

export default ClientComponent;

참고문헌