React 19 cache()로 서버 컴포넌트 워터폴 페칭 막기

원문: Avoiding Server Component Waterfall Fetching with React 19 cache()

서버 컴포넌트에서 데이터 요청이 부모→자식으로 순차적으로 이뤄져 전체 렌더링을 지연시키는 문제가 있는데, 이를 Waterfall Fetching이라 부른다.

해당 게시글은 이 문제를 간단히 짚고, React 19에서 새롭게 제공되는 cache() API를 활용해 이를 최소화하는 방법을 소개한다. 중복 요청을 방지하고 캐싱을 활용해 불필요한 대기 시간을 줄임으로써, 서버 컴포넌트의 성능을 한층 개선할 수 있다는 점이 핵심이다.


📚 React 19 cache()로 서버 컴포넌트 워터폴 페칭 방지하기

1단계: cache() API가 뭔가요?

React 19에서 새로 나온 cache() API는 데이터 페칭이나 계산 결과를 캐싱하는 기능입니다.

🎯 실생활 비유: 마트에서 장 볼 때를 생각해보세요. 가족 구성원 여러 명이 각자 우유가 필요해서 따로따로 마트에 간다면 비효율적이겠죠? cache()는 한 번 사온 우유를 가족이 함께 나눠 쓰는 것과 같습니다.

javascript
const getUser = cache(async (userId: string) => {
  return db.getUser(userId);
});

2단계: 왜 필요한가요? - 워터폴 문제

워터폴 페칭은 데이터를 순차적으로 가져오는 문제입니다.

🌊 워터폴 비유: 계단식 폭포처럼 데이터가 위에서 아래로 차례대로 흐릅니다:

  1. Post 컴포넌트가 데이터를 가져옴 (1초)
  2. Post가 끝나야 Comments가 시작 (또 1초)
  3. 총 2초 소요 😓
javascript
// 문제가 있는 코드
async function Post({ postId }) {
  const post = await getPost(postId);// 1초 대기

  return (
    <div>
      <h2>{post.title}</h2>
      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments postId={postId} /> {/* Post 후에야 시작 */}
      </Suspense>
    </div>
  );
}

3단계: 해결 방법들

방법 1: Promise.all() 사용 (단순하지만 문제 있음)

javascript
const [post, comments] = await Promise.all([
  getPost(postId),
  getComments(postId)
]);

문제점:

  • 컴포넌트 간 결합도 증가
  • Post가 Comments 데이터도 알아야 함
  • 나중에 Comments 제거시 코드 정리 필요

방법 2: cache()로 프리로딩 (추천! ✨)

javascript
// 1. getComments를 cache()로 감싸기
const getComments = cache(async (postId: string) => {
// 데이터 페칭 로직
});

// 2. 상위 컴포넌트에서 미리 호출 (await 없이!)
export async function Post({ postId }) {
  getComments(postId);// 프리로드 시작
  const post = await getPost(postId);

  return (
// Comments는 이미 로딩 중이므로 바로 렌더링
  );
}

4단계: 실제 효과

🚀 성능 개선:

  • 이전: Post(1초) → Comments(1초) = 총 2초
  • 이후: Post와 Comments 동시 시작 = 총 1초

5단계: 주의사항 및 베스트 프랙티스

  1. 숨겨진 결합 문제 해결:
javascript
// Comments 컴포넌트에서 프리로드 함수 export
export const preloadComments = (id: string) => {
  void getComments(id);
};
  1. Next.js fetch()는 이미 캐싱됨:
  • fetch() API 사용시 cache() 불필요
  • DB 직접 호출이나 커스텀 함수에 사용
  1. 언제 사용할까?:
  • ✅ 여러 컴포넌트에서 같은 데이터 필요할 때
  • ✅ 서버 컴포넌트 워터폴 방지할 때
  • ❌ 성능 문제가 없다면 미리 최적화 금지
  1. 컴포지션 vs 퍼포먼스 트레이드오프
    • cache()는 컴포넌트 독립성을 유지하면서도 성능을 개선하는 절묘한 해결책입니다
    • 기존에는 둘 중 하나를 포기해야 했죠
  2. React의 진화 방향
    • React는 개발자 경험(DX)과 성능을 동시에 잡으려 노력하고 있습니다
    • cache()는 이런 철학을 잘 보여주는 API입니다
  3. 실무 적용 팁
    • 먼저 측정하고, 실제 병목이 있을 때만 최적화하세요
    • 프리로딩 패턴은 강력하지만, 코드 복잡도를 높일 수 있습니다
    • 팀원들과 패턴을 공유하고 문서화하세요
  4. Next.js vs 다른 프레임워크
    • React Router v7, TanStack Start는 loader 패턴으로 자동 해결
    • Next.js는 더 명시적인 제어를 제공하는 대신 수동 작업 필요

이 cache() API는 React 서버 컴포넌트의 성능을 크게 개선할 수 있는 도구입니다. 특히 복잡한 데이터 의존성이 있는 대규모 애플리케이션에서 빛을 발할 것 같네요! 😊