본문 바로가기
개발

프론트 필수 라이브러리 모음 : 8화 TanStack Query & SWR - 데이터 통신과 서버 상태 관리🧩✨

by D-Project 2025. 4. 24.

8화. TanStack Query & SWR — 데이터 통신의 꽃🌐

안녕하세요 🌟 오늘은 프론트엔드 필수 라이브러리 시리즈의 여덟 번째 이야기로, 서버 데이터 관리의 두 강자 TanStack Query와 SWR에 대해 알아볼게요! 이 두 라이브러리는 2025년 현재, API 통신과 서버 상태 관리의 표준으로 자리 잡았답니다! 💾

왜 서버 상태 관리가 중요할까요? 🤔

현대 웹 애플리케이션은 서버에서 데이터를 가져와 화면에 표시하는 작업이 핵심이죠. 하지만 이 과정에는 생각보다 많은 복잡성이 숨어 있어요:

  • 캐싱: 같은 데이터를 반복해서 요청하지 않도록 관리
  • 로딩/에러 상태: API 요청의 다양한 상태 처리
  • 데이터 동기화: 서버 데이터 변경 시 UI 자동 갱신
  • 페이지네이션/무한 스크롤: 대량 데이터의 효율적인 로딩
  • 낙관적 업데이트: UI를 즉시 업데이트하고 서버 응답 대기
  • 요청 중복 제거: 동일 데이터에 대한 중복 요청 방지

이런 복잡한 문제들을 직접 구현하려면 많은 코드와 시간이 필요하죠. TanStack Query와 SWR은 이 모든 문제를 우아하게 해결해주는 라이브러리예요! 함께 살펴볼까요?

TanStack Query: 강력한 데이터 동기화의 정석 💪

TanStack Query(이전 명칭: React Query)는 Tanner Linsley가 개발한 라이브러리로, 서버 상태 관리를 위한 강력한 기능을 제공해요. 2025년 현재 TanStack Query는 React뿐만 아니라 Vue, Svelte, Solid, Angular 등 다양한 프레임워크를 지원하는 범용 라이브러리로 발전했답니다!

TanStack Query의 주요 특징:

  1. 선언적 데이터 페칭 📝
    • 복잡한 로직을 간결한 hook으로 추상화
    • 데이터 요청 상태(로딩, 성공, 에러)를 자동으로 관리
  2. 스마트 캐싱 시스템 💾
    • 지능적인 캐싱으로 중복 요청 방지
    • 캐시 무효화와 자동 리페칭(refetching) 제공
  3. 자동 동기화와 백그라운드 업데이트 🔄
    • 창 포커스, 네트워크 재연결 시 자동 데이터 갱신
    • 오래된 데이터를 표시하면서 백그라운드에서 갱신
  4. 페이지네이션과 무한 스크롤 📚
    • 대량 데이터 로딩을 위한 내장 유틸리티
    • 이전 페이지 데이터를 유지하면서 새 페이지 요청
// TanStack Query 기본 사용 예시
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 데이터 페칭 함수
const fetchTodos = async () => {
  const response = await fetch('https://api.example.com/todos');
  if (!response.ok) {
    throw new Error('네트워크 응답이 올바르지 않습니다');
  }
  return response.json();
};

// 할 일 추가 함수
const addTodo = async (newTodo) => {
  const response = await fetch('https://api.example.com/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  });
  return response.json();
};

function TodoList() {
  const queryClient = useQueryClient();

  // 데이터 조회
  const { data, isLoading, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 60000, // 1분 동안 데이터를 최신 상태로 간주
  });

  // 데이터 변경
  const mutation = useMutation({
    mutationFn: addTodo,
    onSuccess: () => {
      // 성공 시 'todos' 쿼리 무효화하여 자동으로 다시 가져오기
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  const handleAddTodo = () => {
    mutation.mutate({ title: '새 할 일', completed: false });
  };

  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>에러: {error.message}</div>;

  return (
    <div>
      <h1>할 일 목록</h1>
      <ul>
        {data.map(todo => (
          <li key={todo.id}>
            {todo.title} {todo.completed ? '✅' : '❌'}
          </li>
        ))}
      </ul>
      <button onClick={handleAddTodo} disabled={mutation.isPending}>
        {mutation.isPending ? '추가 중...' : '할 일 추가'}
      </button>
    </div>
  );
}

 

TanStack Query의 고급 기능:

  1. useQuery의 강력한 옵션들
const { data, isLoading } = useQuery({
  queryKey: ['todos', { status, page }], // 동적 쿼리 키
  queryFn: () => fetchTodos(status, page),
  staleTime: 60000, // 1분 동안 데이터를 신선한 상태로 유지
  gcTime: 300000, // 5분 동안 캐시된 데이터 유지
  refetchOnWindowFocus: true, // 윈도우 포커스 시 자동 리페칭
  retry: 3, // 실패 시 최대 3번 재시도
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), // 지수 백오프
  onSuccess: data => console.log('데이터 로드 성공:', data),
  onError: error => console.error('데이터 로드 실패:', error),
});
  1. 의존적 쿼리 (Dependent Queries)
// 첫 번째 쿼리
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

// 사용자 데이터가 있을 때만 프로젝트 쿼리 실행
const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => fetchProjects(user.id),
  // user가 없으면 쿼리가 실행되지 않음
  enabled: !!user,
});
  1. 무한 쿼리와 페이지네이션
// 무한 스크롤 구현
const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 1 }) => fetchProjects(pageParam),
  getNextPageParam: (lastPage, allPages) => {
    return lastPage.nextPage ?? undefined;
  },
});

// 사용 예
return (
  <div>
    {data.pages.map((page, i) => (
      <React.Fragment key={i}>
        {page.projects.map(project => (
          <p key={project.id}>{project.name}</p>
        ))}
      </React.Fragment>
    ))}
    <button
      onClick={() => fetchNextPage()}
      disabled={!hasNextPage || isFetchingNextPage}
    >
      {isFetchingNextPage
        ? '로딩 중...'
        : hasNextPage
        ? '더 보기'
        : '더 이상 데이터가 없습니다'}
    </button>
  </div>
);
  1. 낙관적 업데이트 (Optimistic Updates)
const queryClient = useQueryClient();

// 낙관적 업데이트로 즉각적인 UI 반응
const mutation = useMutation({
  mutationFn: updateTodo,
  // 변경 전 낙관적으로 UI 업데이트
  onMutate: async (newTodo) => {
    // 진행 중인 관련 쿼리 취소
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });

    // 이전 상태 저장
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);

    // 새 값으로 캐시 업데이트
    queryClient.setQueryData(['todos', newTodo.id], newTodo);

    // 롤백을 위해 이전 값 반환
    return { previousTodo };
  },
  // 에러 발생 시 롤백
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', newTodo.id],
      context.previousTodo
    );
  },
  // 성공 또는 실패 후 갱신
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] });
  },
});

2025년 TanStack Query v6의 최신 기능:

  • 서스펜스 모드 개선: React의 Suspense와 더 자연스러운 통합
  • 자동 타입 추론 강화: TypeScript와의 더 나은 통합으로 타입 안전성 향상
  • 타이머 기반 GC: 성능 최적화를 위한 새로운 가비지 컬렉션 알고리즘
  • 쿼리 그룹화: 관련 쿼리를 그룹으로 관리하고 일괄 처리
  • 액션 관리자: 서버 액션과의 통합을 위한 새로운 유틸리티

SWR: 간결함과 직관성의 대명사 🔄

SWR(Stale-While-Revalidate)은 Vercel 팀이 개발한 데이터 페칭 라이브러리로, HTTP 캐시 무효화 전략에서 이름을 따왔어요. 간결한 API와 직관적인 사용법으로 많은 사랑을 받고 있답니다!

SWR의 주요 특징:

  1. 최소한의 API 🧘‍♀️
    • 단 하나의 핵심 함수 useSWR로 대부분의 기능 제공
    • 간결하고 직관적인 API 설계
  2. 빠른 페이지 전환
    • 캐시된 데이터를 즉시 표시하고 백그라운드에서 갱신
    • 사용자 경험을 극대화하는 설계 철학
  3. 자동 리페칭 🔄
    • 창 포커스, 네트워크 재연결, 일정 간격으로 자동 갱신
    • 항상 최신 데이터 유지 가능
  4. 타입스크립트 지원 📘
    • 완벽한 타입 안정성
    • 자동 타입 추론으로 개발 경험 향상
// SWR 기본 사용 예시
import useSWR from 'swr';

// fetcher 함수
const fetcher = (url) => fetch(url).then(res => {
  if (!res.ok) throw new Error('API 요청 실패');
  return res.json();
});

function Profile() {
  const { data, error, isLoading } = useSWR(
    'https://api.example.com/user',
    fetcher
  );

  if (isLoading) return <div>로딩 중...</div>;
  if (error) return <div>에러: {error.message}</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>직업: {data.profession}</p>
    </div>
  );
}

SWR의 고급 기능:

  1. 조건부 페칭
// 조건부로 데이터 가져오기
function UserProfile({ userId }) {
  const { data } = useSWR(
    userId ? `/api/user/${userId}` : null, 
    fetcher
  );

  // userId가 없으면 요청이 실행되지 않음
  if (!userId) return <div>사용자를 선택하세요</div>;
  if (!data) return <div>로딩 중...</div>;

  return <h1>{data.name}의 프로필</h1>;
}
  1. 데이터 변경과 재검증
import useSWR, { mutate } from 'swr';

function TodoList() {
  const { data } = useSWR('/api/todos', fetcher);

  // 낙관적 UI 업데이트와 재검증
  const addTodo = async (text) => {
    // 1. 현재 캐시된 데이터 가져오기
    const newTodo = { id: Date.now(), text, completed: false };
    const updatedTodos = [...(data || []), newTodo];

    // 2. 낙관적 업데이트를 위해 로컬 데이터 변경
    mutate('/api/todos', updatedTodos, false); // false는 재검증 건너뛰기를 의미

    // 3. 실제 API 호출
    try {
      await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });

      // 4. 성공 시 데이터 재검증
      mutate('/api/todos');
    } catch (error) {
      // 5. 실패 시 원래 데이터로 롤백
      mutate('/api/todos');
    }
  };

  return (
    <div>
      <ul>
        {data?.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <button onClick={() => addTodo('새 할 일')}>추가</button>
    </div>
  );
}
  1. 무한 스크롤
import { useSWRInfinite } from 'swr/infinite';

function InfiniteProjects() {
  // 페이지 키 생성 함수
  const getKey = (pageIndex, previousPageData) => {
    // 끝에 도달
    if (previousPageData && !previousPageData.length) return null;

    // 첫 페이지이면 `previousPageData`는 `null`
    return `/api/projects?page=${pageIndex + 1}&limit=10`;
  };

  const { data, size, setSize, isValidating } = useSWRInfinite(
    getKey,
    fetcher
  );

  // 모든 프로젝트를 하나의 배열로 평탄화
  const projects = data ? [].concat(...data) : [];
  const isEmpty = data?.[0]?.length === 0;
  const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);

  return (
    <div>
      <ul>
        {projects.map(project => (
          <li key={project.id}>{project.name}</li>
        ))}
      </ul>
      <button
        onClick={() => setSize(size + 1)}
        disabled={isReachingEnd || isValidating}
      >
        {isValidating
          ? '로딩 중...'
          : isReachingEnd
          ? '더 이상 없음'
          : '더 보기'}
      </button>
    </div>
  );
}
  1. 데이터 포커스 리페칭
import useSWR from 'swr';

function Dashboard() {
  const { data } = useSWR('/api/dashboard', fetcher, {
    // 옵션 설정
    refreshInterval: 3000, // 3초마다 자동 재검증
    revalidateOnFocus: true, // 창 포커스 시 재검증
    revalidateOnReconnect: true, // 네트워크 재연결 시 재검증
    refreshWhenHidden: false, // 페이지가 숨겨져 있을 때는 자동 재검증 중지
  });

  return <div>{/* 데이터 표시 */}</div>;
}

2025년 SWR v4의 최신 기능:

  • 스마트 인터벌: 사용자 상호작용에 따라 자동으로 갱신 간격 조정
  • 뮤테이션 플러그인: 더 강력한 낙관적 UI 업데이트 기능
  • 확장 가능한 캐시 제공자: 커스텀 스토리지 구현 지원
  • 디버깅 도구 개선: 더 나은 개발자 경험을 위한 DevTools
  • 교차 탭 동기화: 여러 탭 간의 데이터 상태 동기화

TanStack Query vs SWR: 어떤 선택이 좋을까? 🤔

두 라이브러리 모두 훌륭한 선택이지만, 프로젝트의 특성과 팀의 선호도에 따라 선택이 달라질 수 있어요. 주요 차이점을 비교해볼게요!

크기와 번들 사이즈:

  • TanStack Query: 약 12KB (gzip)
  • SWR: 약 5KB (gzip)

API 스타일:

  • TanStack Query: 기능이 풍부하고 세밀한 제어 가능, 다양한 훅 제공
  • SWR: 미니멀하고 직관적인 API, 핵심 기능에 집중

기능 세트:

  • TanStack Query: 더 많은 내장 기능과 미들웨어 제공
  • SWR: 필수 기능에 집중하며 간결한 API 유지

프레임워크 지원:

  • TanStack Query: React, Vue, Svelte, Solid, Angular 등 광범위한 지원
  • SWR: 주로 React에 집중 (Next.js와의 통합이 특히 뛰어남)

사용 시나리오별 추천:

TanStack Query를 선택해야 할 때:

  • 복잡한 데이터 요구사항이 있는 대규모 애플리케이션
  • 페이지네이션, 무한 스크롤 등의 고급 기능이 자주 필요한 경우
  • 다양한 프레임워크를 사용하는 프로젝트
  • 세밀한 캐싱 제어와 다양한 옵션이 필요한 경우

SWR을 선택해야 할 때:

  • 간결한 API와 빠른 학습 곡선을 선호할 때
  • Next.js 프로젝트와의 통합이 중요할 때
  • 번들 크기를 최소화하고 싶을 때
  • 기본 설정으로도 충분한 간단한 데이터 요구사항

실제 사용 사례 💼

실제 프로젝트에서 두 라이브러리를 어떻게 활용할 수 있는지 살펴볼게요!

1. 실시간 대시보드 (TanStack Query)

// TanStack Query로 실시간 대시보드 구현
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';

// API 함수
const fetchDashboardData = async (timeRange) => {
  const response = await fetch(`/api/dashboard?range=${timeRange}`);
  return response.json();
};

function Dashboard() {
  const [timeRange, setTimeRange] = useState('day');
  const queryClient = useQueryClient();

  const { data, isLoading, error } = useQuery({
    queryKey: ['dashboard', timeRange],
    queryFn: () => fetchDashboardData(timeRange),
    refetchInterval: 30000, // 30초마다 자동 갱신
  });

  // 시간 범위 변경 핸들러
  const handleRangeChange = (newRange) => {
    setTimeRange(newRange);
  };

  // 수동 새로고침
  const handleRefresh = () => {
    queryClient.invalidateQueries({ queryKey: ['dashboard', timeRange] });
  };

  if (isLoading) return <div>대시보드 로딩 중...</div>;
  if (error) return <div>에러: {error.message}</div>;

  return (
    <div>
      <h1>실시간 대시보드</h1>

      <div className="controls">
        <select 
          value={timeRange} 
          onChange={(e) => handleRangeChange(e.target.value)}
        >
          <option value="day">오늘</option>
          <option value="week">이번 주</option>
          <option value="month">이번 달</option>
        </select>

        <button onClick={handleRefresh}>새로고침</button>
      </div>

      <div className="stats">
        <div className="stat-card">
          <h3>방문자 수</h3>
          <p className="stat-value">{data.visitors}</p>
        </div>

        <div className="stat-card">
          <h3>판매량</h3>
          <p className="stat-value">{data.sales}</p>
        </div>

        <div className="stat-card">
          <h3>전환율</h3>
          <p className="stat-value">{data.conversionRate}%</p>
        </div>
      </div>

      <div className="chart">
        {/* 차트 컴포넌트 렌더링 */}
        <LineChart data={data.timeSeriesData} />
      </div>
    </div>
  );
}

2. 소셜 미디어 피드 (SWR)

// SWR로 소셜 미디어 피드 구현
import useSWR, { useSWRInfinite, mutate } from 'swr';
import { useState } from 'react';

// API 함수
const fetcher = url => fetch(url).then(res => res.json());

function SocialFeed() {
  const [newPostText, setNewPostText] = useState('');

  // 무한 스크롤 데이터 가져오기
  const getKey = (pageIndex, previousPageData) => {
    if (previousPageData && !previousPageData.posts.length) return null;
    return `/api/posts?page=${pageIndex + 1}&limit=10`;
  };

  const { data, size, setSize, isValidating } = useSWRInfinite(
    getKey,
    fetcher
  );

  // 사용자 정보 가져오기
  const { data: userData } = useSWR('/api/user', fetcher);

  // 모든 포스트를 하나의 배열로 평탄화
  const posts = data ? data.flatMap(page => page.posts) : [];
  const isReachingEnd = data && data[data.length - 1]?.posts.length < 10;

  // 새 포스트 작성
  const handlePostSubmit = async (e) => {
    e.preventDefault();
    if (!newPostText.trim()) return;

    // 낙관적 업데이트
    const newPost = {
      id: Date.now(),
      text: newPostText,
      user: userData,
      likes: 0,
      createdAt: new Date().toISOString(),
    };

    // 첫 페이지 데이터 가져오기
    const firstPageKey = getKey(0, null);
    const firstPageData = data?.[0] || { posts: [] };

    // 새 포스트를 맨 위에 추가
    mutate(
      firstPageKey,
      { ...firstPageData, posts: [newPost, ...firstPageData.posts] },
      false
    );

    // 입력 필드 초기화
    setNewPostText('');

    try {
      // 실제 API 호출
      await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: newPostText }),
      });

      // 성공 시 첫 페이지 데이터 재검증
      mutate(firstPageKey);
    } catch (error) {
      console.error('포스트 작성 실패:', error);
      // 실패 시 원래 데이터로 롤백 (자동으로 재검증)
      mutate(firstPageKey);
    }
  };

  // 좋아요 토글
  const handleLikeToggle = async (postId) => {
    // 모든 페이지에서 해당 포스트 업데이트
    const updatePost = (pageData) => {
      return {
        ...pageData,
        posts: pageData.posts.map(post => {
          if (post.id === postId) {
            const liked = post.liked || false;
            return {
              ...post,
              liked: !liked,
              likes: liked ? post.likes - 1 : post.likes + 1,
            };
          }
          return post;
        }),
      };
    };

    // 각 페이지 데이터 업데이트
    for (let i = 0; i < data.length; i++) {
      const pageKey = getKey(i, i > 0 ? data[i-1] : null);
      mutate(pageKey, updatePost(data[i]), false);
    }

    try {
      // 실제 API 호출
      await fetch(`/api/posts/${postId}/like`, {
        method: 'POST',
      });

      // 모든 데이터 재검증
      mutate('/api/posts');
    } catch (error) {
      console.error('좋아요 토글 실패:', error);
      // 실패 시 자동으로 재검증됨
    }
  };

  return (
    <div className="social-feed">
      <h1>소셜 피드</h1>

      {/* 새 포스트 작성 폼 */}
      <form onSubmit={handlePostSubmit} className="post-form">
        <textarea
          value={newPostText}
          onChange={(e) => setNewPostText(e.target.value)}
          placeholder="무슨 생각을 하고 계신가요?"
          maxLength={280}
        />
        <button type="submit">게시</button>
      </form>

      {/* 포스트 목록 */}
      <div className="posts-list">
        {posts.map(post => (
          <div key={post.id} className="post-card">
            <div className="post-header">
              <img src={post.user.avatar} alt={post.user.name} />
              <div>
                <h3>{post.user.name}</h3>
                <time>{new Date(post.createdAt).toLocaleString()}</time>
              </div>
            </div>

            <p className="post-text">{post.text}</p>

            <div className="post-actions">
              <button 
                className={`like-button ${post.liked ? 'liked' : ''}`}
                onClick={() => handleLikeToggle(post.id)}
              >
                {post.liked ? '♥' : '♡'} {post.likes}
              </button>
            </div>
          </div>
        ))}

        {/* 더 보기 버튼 */}
        <button
          onClick={() => setSize(size + 1)}
          disabled={isReachingEnd || isValidating}
          className="load-more-button"
        >
          {isValidating
            ? '로딩 중...'
            : isReachingEnd
            ? '더 이상 포스트가 없습니다'
            : '더 보기'}
        </button>
      </div>
    </div>
  );
}

서버 상태 관리의 베스트 프랙티스 🌟

어떤 라이브러리를 선택하든 효과적인 서버 상태 관리를 위한 몇 가지 권장 사항이 있어요!

1. 적절한 캐싱 전략 수립

// TanStack Query에서 캐싱 설정
const queryOptions = {
  staleTime: 60 * 1000, // 1분 동안 데이터를 신선하게 유지
  gcTime: 5 * 60 * 1000, // 5분 동안 비활성 캐시 유지
};

// SWR에서 캐싱 설정
const swrOptions = {
  dedupingInterval: 2000, // 2초 내 중복 요청 방지
  revalidateOnFocus: false, // 특정 경우 자동 재검증 끄기
};

2. 에러 처리와 재시도

// TanStack Query의 에러 처리 및 재시도
const { data, error } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  retry: (failureCount, error) => {
    // 특정 에러에 대해서만 재시도
    if (error.status === 404) return false; // 404는 재시도 안 함
    return failureCount < 3; // 다른 에러는 최대 3번 재시도
  },
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});

// SWR의 에러 처리 및 재시도
const { data, error } = useSWR('/api/data', fetcher, {
  errorRetryCount: 3, // 최대 3번 재시도
  errorRetryInterval: 5000, // 5초 간격으로 재시도
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // 404는 재시도 안 함
    if (error.status === 404) return;

    // 최대 10번까지만 재시도
    if (retryCount >= 10) return;

    // 5초 후 재시도
    setTimeout(() => revalidate({ retryCount }), 5000);
  },
});

3. 백그라운드 데이터 동기화 조정

// TanStack Query 백그라운드 동기화 설정
const { data } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  refetchOnWindowFocus: 'always', // 항상 창 포커스 시 리페칭
  refetchOnMount: false, // 마운트 시 리페칭 비활성화
  refetchOnReconnect: true, // 네트워크 재연결 시 리페칭
  refetchInterval: isUserActive ? 30000 : false, // 사용자 활성 시에만 30초마다 리페칭
});

// SWR 백그라운드 동기화 설정
const { data } = useSWR('/api/user', fetcher, {
  revalidateOnFocus: true,
  revalidateIfStale: true,
  revalidateOnReconnect: true,
  refreshInterval: isUserActive ? 30000 : 0,
  refreshWhenHidden: false, // 페이지가 숨겨져 있을 때는 리페칭 중지
});

4. 낙관적 업데이트와 롤백

// 낙관적 업데이트 패턴
const handleUpdateUser = async (newData) => {
  // 1. 현재 데이터 저장
  const previousData = queryClient.getQueryData(['user']);

  // 2. 낙관적 업데이트
  queryClient.setQueryData(['user'], old => ({ ...old, ...newData }));

  try {
    // 3. 실제 API 호출
    await updateUser(newData);

    // 4. 성공 시 서버 데이터로 재검증
    queryClient.invalidateQueries({ queryKey: ['user'] });
  } catch (error) {
    // 5. 실패 시 이전 데이터로 롤백
    queryClient.setQueryData(['user'], previousData);

    // 6. 사용자에게 에러 알림
    toast.error('사용자 정보 업데이트에 실패했습니다.');
  }
};

마무리 🎁

TanStack Query와 SWR은 모두 현대 웹 애플리케이션의 데이터 페칭과 서버 상태 관리를 크게 개선해주는 강력한 도구예요! 두 라이브러리는 각자의 장점이 있으며, 프로젝트의 특성과 팀의 선호도에 따라 적절한 선택을 할 수 있어요.

  • TanStack Query: 더 많은 기능과 세밀한 제어가 필요한 복잡한 프로젝트에 적합
  • SWR: 간결함과 직관성을 중시하는 프로젝트에 적합

한 가지 중요한 점은, 이 두 라이브러리는 클라이언트 상태(예: 폼 상태, UI 상태)를 관리하는 것이 아니라 서버 상태(API에서 가져온 데이터)를 관리하는 데 특화되어 있다는 것이에요! 클라이언트 상태 관리는 앞서 살펴본 Zustand, Jotai, Redux 등의 라이브러리가 담당하죠.

다음 시간에는 프론트엔드 개발자의 생산성을 높여주는 유틸리티 라이브러리들인 Lodash, Day.js, Lucide에 대해 알아볼 예정이니 기대해주세요! 🧰

여러분의 서버 상태 관리가 더욱 효율적이고 즐거워지길 바랍니다! 😄👋