[Next.js] SSR 환경에서의 데이터 공유(캐싱, 전역 상태 관리)

2025. 4. 20. 15:11·Next.js

프로젝트 중 Next.js에서 서버 컴포넌트인 최상단의 페이지 컴포넌트에서, 클라이언트 컴포넌트인 자식 컴포넌트에게 데이터를 어떻게 넘겨줄 수 있을지에 대한 고민을 했다. 우선 전역 상태관리로 관리하는 데이터는 서버 컴포넌트에서 불러올 수 없다. (!!)

그럼 첫 번째 방법은, React Server Component Payload (RSC)를 활용하는 것이다.

import ClientComponent from './client-component';
import { fetchUserData } from './data-fetching';

export default async function ServerComponent() {
  const userData = await fetchUserData();
  
  return <ClientComponent userData={userData} />;
}

이렇게 서버 컴포넌트에서 api를 호출하는 함수를 실행시켜서 props로 넘겨준다. 가장 기본적인? 방법이다.

그럼 저 데이터를 활용하는 서버 컴포넌트를 띄울 때마다 매번 api를 호출해야되나?

서버 컴포넌트 간에 데이터를 공유할 수 있는 방법도 있다. 바로 React cache를 활용하는 것이다. 

이 함수는 오직 서버 컴포넌트에서만 작동하며, 클라이언트 컴포넌트에서는 사용할 수 없다.

// utils/user-data.js
import { cache } from 'react';

export const getUserData = cache(async () => {
  // 데이터 가져오기 로직
  return userData;
});

// 서버 컴포넌트 1
import { getUserData } from '../utils/user-data';

export default async function ServerComponent1() {
  const userData = await getUserData();
  // ...
}

// 서버 컴포넌트 2
import { getUserData } from '../utils/user-data';

export default async function ServerComponent2() {
  const userData = await getUserData();
  // ...
}

이 방법을 사용하면 여러 서버 컴포넌트에서 동일한 데이터를 가져올 때 중복 요청을 방지할 수 있다. 여러 컴포넌트에서 같은 데이터를 요청해도 실제 네트워크 요청은 한 번만 발생한다.

다른 방법으로는 Next.js의 fetch 사용 시 데이터 캐싱 방법이 있다. Next.js에서는 fetch를 사용하면 자동으로 데이터를 캐싱하는데, revalidate 옵션을 사용해서 데이터의 유효 시간을 설정할 수 있다. 설정된 시간이 지나면 데이터가 stale 상태로 변경된다.

// 캐싱을 활용한 fetch
async function getData() {
  const res = await fetch('https://api.example.com/data', { 
    next: { revalidate: 3600 } // 1시간마다 재검증
  });
  
  return res.json();
}

그리고 next.tags 옵션을 사용하면 관련 데이터를 그룹화해 필요할 때 한 번에 다시 가져올 수 있다. 새 게시물을 생성하는 등 특정 이벤트가 발생할 때 관련 데이터를 모두 다시 가져오는 데 유용하다.

// 태그 지정
async function fetchPosts() {
  const response = await fetch('https://api.example.com/posts', { 
    next: { tags: ['posts'] } 
  });
  return response.json();
}

// 서버 액션에서 태그 기반 재검증
import { revalidateTag } from 'next/cache';

export async function updatePost() {
  // 데이터 업데이트 로직...
  
  // 'posts' 태그가 지정된 모든 데이터 재검증
  revalidateTag('posts');
  
  return { success: true };
}

그리고 아래와 같이 경로 기반으로도 가능하다.

import { revalidatePath } from 'next/cache';

export async function updatePost() {
  // 데이터 업데이트 로직...
  
  // '/blog' 경로와 관련된 모든 데이터 재검증
  revalidatePath('/blog');
  
  return { success: true };
}

또 다른 방법으로는 unstable_cache API를 활용한 데이터베이스 쿼리 결과 캐싱이 있다. 이 방법은 Next.js 14에서 도입되었다.

import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';

// 데이터베이스 쿼리 결과 캐싱
const getCachedPosts = unstable_cache(
  async () => {
    console.log('데이터베이스 쿼리 실행'); // 캐시 미스 시에만 실행됨
    return db.posts.findMany();
  },
  ['posts-cache'], // 캐시 키
  {
    revalidate: 3600, // 1시간 캐시
    tags: ['posts']  // 캐시 태그
  }
);

export default async function BlogPage() {
  const posts = await getCachedPosts();
  return <PostList posts={posts} />;
}

unstable_cache의 주요 매개변수는 아래와 같다.

1. fetchData: 캐싱할 데이터를 가져오는 비동기 함수

2. keyParts: 캐시 키를 구성하는 문자열 배열 (캐시 항목을 고유하게 식별하는 데 사용)

3. options: 캐싱 동작을 제어하는 옵션 객체

    - revalidate: 캐시 유효 시간(초)

    - tags: 캐시 항목에 연결할 태그 배열

함수 인자에 따라 다른 캐시 결과를 얻고 싶을 땐 아래와 같이 사용할 수도 있다. 풀스택 프레임워크답다.

import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';

// 사용자 ID에 따라 다른 데이터 캐싱
const getCachedUserPosts = unstable_cache(
  async (userId) => {
    console.log(`사용자 ${userId}의 게시물 조회`);
    return db.posts.findMany({ where: { authorId: userId } });
  },
  ['user-posts'], // 기본 캐시 키
  { 
    revalidate: 60, // 1분 캐시
    tags: ['user-posts'] 
  }
);

export default async function UserPostsPage({ params }) {
  const { userId } = params;
  // userId가 함수에 전달되어 해당 사용자의 데이터만 캐싱
  const posts = await getCachedUserPosts(userId);
  return <PostList posts={posts} />;
}

데이터를 새롭게 가져오고 싶을 땐 아래와 같이 캐시를 무효화하면 된다.

import { revalidateTag } from 'next/cache';

export async function createPost(formData) {
  'use server';
  
  // 새 게시물 생성 로직...
  
  // 'posts' 태그가 지정된 모든 캐시 항목 무효화
  revalidateTag('posts');
  
  // 'user-posts' 태그가 지정된 모든 캐시 항목 무효화
  revalidateTag('user-posts');
  
  return { success: true };
}

 

fetch 캐싱은 HTTP 요청에 적합하고, unstable_cache는 데이터베이스 쿼리와 같은 비 HTTP 데이터 소스에 적합하다.


지금까지는 React-Query를 이용하지 않고도 Next.js의 내장 데이터를 효율적으로 관리할 수 있는 방법을 알아보았다.

그럼 React-Query를 이용해서 서버 컴포넌트 -> 클라이언트 컴포넌트로 데이터를 전달하는 방법은 무엇이 있을까?

바로 prefetch를 사용해서 dehydrate를 한 후 hydrationBoundary로 클라이언트 컴포넌트를 감싸는 것이다.

그럼 그 클라이언트 컴포넌트에서는 프리페칭된 데이터를 별도의 api 호출 없이 즉시 사용할 수 있다.

이 방법은 서버 컴포넌트와 클라이언트 컴포넌트의 장점을 모두 활용하는 하이브리드 접근법이다.

// app/posts/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import PostList from './post-list' // 클라이언트 컴포넌트

// 데이터 가져오기 함수
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const queryClient = new QueryClient()
  
  // 서버에서 데이터 프리페칭
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <PostList />
    </HydrationBoundary>
  )
}

무한 스크롤같은 경우, 첫 페이지만 서버에서 프리페칭하고 클라이언트에서 추가 페이지를 선택적으로 로드할 수 있도록 하는 방식을 이용해 최적화할 수 있다.

그런데 서버 컴포넌트에서는 useQuery와 같은 훅을 직접 사용할 수 없기 때문에(클라이언트에서만 작동) 필요시 위에서 봤던 React cache를 함께 이용할 수 있다.

React Query를 서버 컴포넌트와 함께 사용할 때는, 서버 컴포넌트는 주로 데이터 프리페칭에 사용하고, 실제 상태 관리와 데이터 조작은 클라이언트 컴포넌트에서 처리하는 것이 좋다. 이렇게 하면 서버 렌더링의 성능 이점과 클라이언트 측 상태 관리의 유연성을 모두 활용할 수 있게 된다.

 

참고

더보기

https://velog.io/@nyoung113/React-Query-Next.js-app-router

https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr

https://soobing.github.io/react/next-app-router-react-query/

저작자표시 비영리 변경금지 (새창열림)

'Next.js' 카테고리의 다른 글

[Next.js & React-Query] 서버 컴포넌트에서 QueryClient 호출하기  (1) 2025.04.28
[Next.js] 프로젝트 초기 세팅을 해보자!  (0) 2025.04.06
React는 라이브러리, Next.js는 프레임워크?  (0) 2025.03.23
[리팩토링]Next.js 프로젝트 리팩토링 기록 정리  (0) 2025.02.27
'Next.js' 카테고리의 다른 글
  • [Next.js & React-Query] 서버 컴포넌트에서 QueryClient 호출하기
  • [Next.js] 프로젝트 초기 세팅을 해보자!
  • React는 라이브러리, Next.js는 프레임워크?
  • [리팩토링]Next.js 프로젝트 리팩토링 기록 정리
버그잡는고양이발
버그잡는고양이발
주니어 개발자입니다!
  • 버그잡는고양이발
    지극히평범한개발블로그
    버그잡는고양이발
  • 전체
    오늘
    어제
    • 분류 전체보기 (382)
      • React (16)
      • Next.js (5)
      • Javascript (5)
      • Typescript (4)
      • Node.js (2)
      • Cs (16)
      • 트러블 슈팅 (5)
      • Html (1)
      • Css (3)
      • Django (0)
      • vue (0)
      • Java (2)
      • Python (0)
      • 독서 (1)
      • 기타 (3)
      • 백준 (192)
      • swea (31)
      • 프로그래머스 (30)
      • 이코테 (4)
      • 99클럽 코테 스터디 (30)
      • ssafy (31)
      • IT기사 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 인기 글

  • 태그

    코딩테스트준비
    99클럽
    항해99
    개발자취업
    Til
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
버그잡는고양이발
[Next.js] SSR 환경에서의 데이터 공유(캐싱, 전역 상태 관리)
상단으로

티스토리툴바