OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 7 / 90
stack-analysis
CHAPTER 7 / 90
읽기 약 2
FUNCTION

데이터 페칭: TanStack Query 실전


핵심 개념

useQuery·useMutation·캐싱·낙관적 업데이트 — API 목록·무한 스크롤·좋아요.

본문

기본 useQuery

TYPESCRIPT📋 코드 (26줄)
// pip install @tanstack/react-query
import { useQuery } from '@tanstack/react-query';

function ProductList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const res = await fetch('/api/products');
      if (!res.ok) throw new Error('Failed');
      return res.json();
    },
    staleTime: 1000 * 60 * 5,  // 5분간 fresh
    gcTime: 1000 * 60 * 30,     // 30분 후 GC
  });

  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>오류: {(error as Error).message}</div>;

  return (
    <ul>
      {data.map((p: Product) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

useMutation + 낙관적 업데이트

TYPESCRIPT📋 코드 (49줄)
import { useMutation, useQueryClient } from '@tanstack/react-query';

function LikeButton({ postId }: { postId: string }) {
  const qc = useQueryClient();

  const { mutate, isPending } = useMutation({
    mutationFn: async ({ postId, liked }: { postId: string; liked: boolean }) => {
      const method = liked ? 'POST' : 'DELETE';
      const res = await fetch(`/api/posts/${postId}/like`, { method });
      if (!res.ok) throw new Error('Failed');
      return res.json();
    },

    // 낙관적 업데이트 — 즉시 UI 변경
    onMutate: async ({ postId, liked }) => {
      await qc.cancelQueries({ queryKey: ['post', postId] });
      const prev = qc.getQueryData(['post', postId]);

      qc.setQueryData(['post', postId], (old: any) => ({
        ...old,
        liked,
        likesCount: old.likesCount + (liked ? 1 : -1),
      }));

      return { prev };
    },

    // 실패 시 롤백
    onError: (err, vars, context) => {
      qc.setQueryData(['post', vars.postId], context?.prev);
    },

    // 성공·실패 모두 후 — 서버 데이터로 동기화
    onSettled: (data, err, vars) => {
      qc.invalidateQueries({ queryKey: ['post', vars.postId] });
    },
  });

  const post = qc.getQueryData<Post>(['post', postId])!;

  return (
    <button
      onClick={() => mutate({ postId, liked: !post.liked })}
      disabled={isPending}
    >
      {post.liked ? '❤️' : '🤍'} {post.likesCount}
    </button>
  );
}

무한 스크롤 — useInfiniteQuery

TYPESCRIPT📋 코드 (51줄)
import { useInfiniteQuery } from '@tanstack/react-query';

function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: async ({ pageParam = 0 }) => {
      const res = await fetch(`/api/posts?cursor=${pageParam}`);
      return res.json();  // { items: Post[], nextCursor: number | null }
    },
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  return (
    <div>
      {data?.pages.flatMap(page =>
        page.items.map((post: Post) => <PostCard key={post.id} post={post} />)
      )}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? '로드 중...' : hasNextPage ? '더 보기' : '끝'}
      </button>
    </div>
  );
}


// IntersectionObserver로 자동 로드
import { useEffect, useRef } from 'react';

function useInfiniteScroll(callback: () => void) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => entry.isIntersecting && callback(),
      { threshold: 1.0 }
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, [callback]);

  return ref;
}

QueryClient 설정

TYPESCRIPT📋 코드 (24줄)
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,
      gcTime: 1000 * 60 * 5,
      retry: 1,
      refetchOnWindowFocus: false,  // 모바일 적합
    },
  },
});

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

다음 챕터

CH.8 "폼 관리: React Hook Form + Zod" — 검증의 표준.


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 데이터 페칭 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

데이터 페칭 관련 인기 라이브러리/패턴 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 데이터 페칭
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 프론트엔드 시장의
데이터 페칭 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
데이터 페칭: TanStack Query 실전 이 3가지만 확실히 잡으세요
1.TanStack Query는 "서버 상태"의 표준 — 캐싱·중복 제거·자동 재요청 자동
2.낙관적 업데이트로 UI 즉시 반영 + 실패 시 롤백 = 빠른 UX
3.다음 챕터 CH.8에서 React Hook Form + Zod — 폼의 표준


공유하기
진행도 7 / 90