서론
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을 사용해봐야겠음!
'프론트엔드 > React' 카테고리의 다른 글
| (React) Virtual DOM과 Reconciliation: React의 렌더링 최적화 원리 (0) | 2025.12.08 |
|---|---|
| (React) Custom Hooks 만들기 (0) | 2025.12.02 |
| (React) Framer Motion 애니메이션 만들어보기 (0) | 2025.12.01 |
| (React) useMemo vs useCallback 차이 (0) | 2025.11.20 |
| (React) props drilling 문제와 해결법 (0) | 2025.11.18 |