프론트엔드/React

(React) React Query vs SWR vs 직접 fetch

그린티_ 2025. 12. 8. 17:51
반응형

서론

React 프로젝트를 하다 보니 API 호출이 정말 많았음
useEffect에서 fetch로 데이터를 받고, 로딩 상태, 에러 상태를 관리하고... "매번 이렇게 해야 하나?" 싶었음
그리고 데이터를 받은 후에 같은 데이터를 또 받으려고 하면 불필요하게 API를 또 호출하더라
"이미 받은 데이터인데 왜 다시 받아?"라는 생각이 들었음, 그러다가 React Query와 SWR 같은 라이브러리를 발견했음
이들이 어떻게 다르고, 언제 뭘 써야 하는지 정리해봄

본론

문제 1: 반복되는 데이터 페칭 코드

// 모든 컴포넌트에서 이렇게 해야 함...
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

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

  return <div>{user.name}</div>;
}

// 다른 컴포넌트에서도...
function UserPosts({ userId }) {
  const [posts, setPosts] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 동일한 패턴 반복...
  useEffect(() => {
    // ...
  }, [userId]);

  // ...
}

문제점:

  • 로딩, 에러, 재시도 로직을 매번 작성
  • 같은 유저 데이터를 여러 컴포넌트에서 요청하면 불필요하게 API 호출
  • 데이터 캐싱이 없음

문제 2: 캐싱의 부재

// 문제 상황
function App() {
  const [activeTab, setActiveTab] = useState('profile');

  return (
    <div>
      <button onClick={() => setActiveTab('profile')}>프로필</button>
      <button onClick={() => setActiveTab('posts')}>게시물</button>

      {activeTab === 'profile' && <UserProfile userId={1} />}
      {activeTab === 'posts' && <UserPosts userId={1} />}
    </div>
  );
}

// 프로필 탭: 유저 데이터 API 호출 (1번)
// 게시물 탭: 게시물 데이터 API 호출 + 유저 데이터 API 호출 (2번)
// 프로필 탭 다시: 유저 데이터 API 호출 또 함 (3번)
// → 같은 데이터를 3번이나 받음! 낭비!

방법 1: 직접 fetch + Custom Hook (기본)

기본적인 패턴을 먼저 이해하자

// hooks/useFetch.ts
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json = await response.json();

        if (isMounted) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

// 사용
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

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

  return <div>{user.name}</div>;
}

장점:

  • 코드가 짧음
  • 추가 라이브러리 필요 없음

단점:

  • 캐싱 없음 (같은 데이터를 여러 번 받음)
  • 재시도 로직 없음
  • 백그라운드 리페칭 없음
  • 로딩 상태 중복

방법 2: React Query (강력하고 완벽)

React Query는 서버 상태 관리 라이브러리
캐싱, 재시도, 백그라운드 리페칭 등이 자동으로 됨

설치:

npm install @tanstack/react-query

기본 사용:

import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';

// 1. QueryClient 생성
const queryClient = new QueryClient();

// 2. App에서 Provider로 감싸기
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MainApp />
    </QueryClientProvider>
  );
}

// 3. useQuery로 데이터 페칭
function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['users', userId], // 캐시 키
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error('Failed');
      return response.json();
    }
  });

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

  return <div>{user.name}</div>;
}

// 같은 userId로 다른 컴포넌트에서도 호출
function UserEmail({ userId }) {
  const { data: user } = useQuery({
    queryKey: ['users', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    }
  });

  // React Query가 캐시에서 데이터 재사용!
  // API 호출 안 함!
  return <div>{user.email}</div>;
}

React Query의 자동 기능:

function UserProfile({ userId }) {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['users', userId],
    queryFn: async () => {
      console.log('API 호출'); // 처음 1번만 실행
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    },
    staleTime: 1000 * 60 * 5, // 5분 동안 캐시 유효
    cacheTime: 1000 * 60 * 10, // 10분 동안 메모리 유지
    retry: 3, // 실패하면 3번 재시도
    refetchInterval: 1000 * 30 // 30초마다 백그라운드에서 리페칭
  });

  return (
    <div>
      <div>{data?.name}</div>
      <button onClick={() => refetch()}>새로고침</button>
    </div>
  );
}

React Query의 강력함: 뮤테이션 (데이터 변경)

import { useMutation, useQueryClient } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const queryClient = useQueryClient();

  // 데이터 변경 뮤테이션
  const updateUserMutation = useMutation({
    mutationFn: async (newName) => {
      const response = await fetch(`/api/users/${userId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: newName })
      });
      return response.json();
    },
    // 뮤테이션 성공 후 캐시 무효화
    onSuccess: (data) => {
      queryClient.invalidateQueries(['users', userId]);
    }
  });

  return (
    <div>
      <button
        onClick={() => updateUserMutation.mutate('새 이름')}
        disabled={updateUserMutation.isPending}
      >
        {updateUserMutation.isPending ? '저장 중...' : '이름 저장'}
      </button>
    </div>
  );
}

React Query 장점:

  • 자동 캐싱 (같은 데이터 중복 호출 방지)
  • 자동 재시도
  • 백그라운드 리페칭
  • staleTime으로 캐시 제어
  • 무효화(invalidation)로 쉬운 업데이트
  • DevTools로 쿼리 상태 확인
  • 무한 쿼리(Infinite Query) 지원

React Query 단점:

  • 번들 크기 큼 (~40kb)
  • 학습 곡선이 있음

방법 3: SWR (간단하고 가벼움)

SWR은 "stale-while-revalidate" 패턴의 라이브러리
React Query보다 간단함

설치:

npm install swr

기본 사용:

import useSWR from 'swr';

// fetcher 정의
const fetcher = (url) => fetch(url).then(r => r.json());

function UserProfile({ userId }) {
  // 이게 다임! 매우 간단
  const { data: user, error, isLoading } = useSWR(
    `/api/users/${userId}`,
    fetcher
  );

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

  return <div>{user.name}</div>;
}

// 같은 URL로 다른 컴포넌트에서도 호출
function UserEmail({ userId }) {
  const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
  // SWR이 캐시에서 데이터 재사용!
  return <div>{user.email}</div>;
}

SWR의 특징: 자동으로 캐시 유효성 검사

function UserProfile({ userId }) {
  const { data, error, isValidating, mutate } = useSWR(
    `/api/users/${userId}`,
    fetcher,
    {
      revalidateOnFocus: true, // 윈도우 포커스 시 리페칭
      revalidateOnReconnect: true, // 네트워크 재연결 시 리페칭
      dedupingInterval: 2000, // 2초 내 중복 요청 제거
      focusThrottleInterval: 5000 // 5초 내 재요청 1번만
    }
  );

  return (
    <div>
      <div>{data?.name}</div>
      {isValidating && <span>업데이트 중...</span>}
      <button onClick={() => mutate()}>새로고침</button>
    </div>
  );
}

SWR의 강점: 뮤테이션 (낙관적 업데이트)

function UserProfile({ userId }) {
  const { data: user, mutate } = useSWR(
    `/api/users/${userId}`,
    fetcher
  );

  const updateName = async (newName) => {
    // 1. 먼저 UI 업데이트 (낙관적 업데이트)
    mutate({ ...user, name: newName }, false);

    // 2. 그 다음 API 호출
    try {
      const response = await fetch(`/api/users/${userId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: newName })
      });
      const newData = await response.json();

      // 3. 서버 응답으로 업데이트
      mutate(newData);
    } catch (error) {
      // 4. 실패하면 원래대로 롤백
      mutate(user);
    }
  };

  return (
    <button onClick={() => updateName('새 이름')}>
      이름 변경
    </button>
  );
}

SWR 장점:

  • 매우 간단한 문법
  • 번들 크기 작음 (~9kb)
  • 낙관적 업데이트 쉬움
  • 자동 재검증
  • 학습 곡선 낮음

SWR 단점:

  • React Query보다 기능이 적음
  • 복잡한 캐시 제어 어려움
  • \무한 쿼리 기능 약함

세 가지 방식 비교

항목 직접 fetch React Query SWR
번들 크기 0kb ~40kb ~9kb
학습 곡선 낮음 중간 낮음
자동 캐싱
자동 재시도
백그라운드 리페칭
무효화 부분
뮤테이션 직접 작성 완벽 지원 좋음
DevTools
무한 쿼리 약함
복잡한 프로젝트 어려움 최고 가능

결론

API 통신은 현대 웹 애플리케이션의 핵심

직접 fetch: 간단하지만 비효율
React Query: 복잡하지만 강력하고 완벽
SWR: 간단하면서도 충분한 기능

요즘 트렌드는 React Query나 SWR을 쓰는 것, 번들 크기와 프로젝트 복잡도에 따라 선택하면 됨

로또시뮬레이션 같은 데이터 캐싱이 중요한 프로젝트에선 React Query가 최고!
간단한 프로젝트면 SWR도 충분!

앞으로 API 통신이 필요할 땐 직접 fetch가 아니라 React Query나 SWR을 사용해봐야겠음!

반응형