웹 애플리케이션을 개발할 때 가장 기본적이면서도 중요한 결정 중 하나는 “어떤 HTTP 클라이언트를 사용할 것인가?”입니다. 브라우저 내장 fetch() API를 사용할 것인가, 아니면 Axios, Ky, SWR, TanStack Query 같은 써드파티 라이브러리를 선택할 것인가?
각 도구는 고유한 장단점과 적합한 사용 사례를 가지고 있으며, 잘못된 선택은 프로젝트의 복잡도를 불필요하게 증가시킬 수 있습니다. 이 가이드에서는 각 솔루션의 특징을 실제 코드와 함께 상세히 비교하여, 여러분의 프로젝트에 최적의 선택을 할 수 있도록 돕겠습니다.
Native fetch() API 완벽 이해
fetch()란 무엇인가?
fetch()는 현대 브라우저에 내장된 네이티브 API로, 네트워크 요청을 처리하는 표준 방법입니다. Promise 기반으로 동작하며, 별도의 라이브러리 설치 없이 사용할 수 있습니다.
// 가장 기본적인 fetch 사용법
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// async/await를 사용한 현대적인 방법
async function getData() {
try {
const response = await fetch('https://api.example.com/data');
// ⚠️ 중요: fetch는 네트워크 에러만 catch로 잡음
// HTTP 에러 (404, 500 등)는 수동으로 체크해야 함
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
Next.js에서 확장된 fetch()
Next.js는 fetch() API를 확장하여 캐싱과 재검증 기능을 추가했습니다:
// Next.js의 확장 fetch - 캐싱과 재검증 옵션
async function getProducts() {
const response = await fetch('https://api.example.com/products', {
// Next.js 전용 옵션
next: {
revalidate: 3600, // 1시간마다 재검증
tags: ['products'] // 태그 기반 캐시 무효화
}
});
return response.json();
}
// 캐싱 비활성화
async function getRealTimeData() {
const response = await fetch('https://api.example.com/live-data', {
cache: 'no-store' // 매번 새로운 데이터 가져오기
});
return response.json();
}
fetch()의 한계와 해결책
fetch()는 간단하지만 실제 프로덕션에서는 많은 추가 작업이 필요합니다:
// 실전에서 필요한 fetch 래퍼 함수
class FetchWrapper {
private baseURL: string;
private defaultHeaders: HeadersInit;
constructor(baseURL: string = '') {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
// 타임아웃 기능 추가 (fetch는 기본적으로 타임아웃이 없음!)
private async fetchWithTimeout(
url: string,
options: RequestInit = {},
timeout: number = 10000
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
console.log(`⏱️ Request timeout after ${timeout}ms`);
}, timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}
// 재시도 로직 추가
private async fetchWithRetry(
url: string,
options: RequestInit = {},
retries: number = 3
): Promise<Response> {
for (let i = 0; i < retries; i++) {
try {
console.log(`🔄 시도 ${i + 1}/${retries}: ${url}`);
const response = await this.fetchWithTimeout(url, options);
// 성공하면 즉시 반환
if (response.ok) {
console.log(`✅ 성공: ${url}`);
return response;
}
// 5xx 에러는 재시도, 4xx는 즉시 실패
if (response.status < 500) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 마지막 시도가 아니면 대기 후 재시도
if (i < retries - 1) {
const delay = Math.pow(2, i) * 1000; // 지수 백오프
console.log(`⏳ ${delay}ms 대기 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
if (i === retries - 1) throw error;
}
}
throw new Error('Max retries exceeded');
}
// 통합 request 메서드
async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = this.baseURL + endpoint;
console.log(`📡 Request: ${options.method || 'GET'} ${url}`);
const response = await this.fetchWithRetry(url, {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
});
// 에러 처리
if (!response.ok) {
const errorBody = await response.text();
console.error(`❌ Error: ${response.status} - ${errorBody}`);
throw new Error(errorBody || `HTTP ${response.status}`);
}
// 응답 파싱
const data = await response.json();
console.log(`✅ Response received`);
return data;
}
// 편의 메서드들
get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' });
}
post<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
// 사용 예시
const api = new FetchWrapper('https://api.example.com');
const users = await api.get<User[]>('/users');
const newUser = await api.post<User>('/users', { name: 'John' });
fetch()의 주요 한계:
- 타임아웃 기능 없음
- 자동 재시도 없음
- 요청/응답 인터셉터 없음
- 진행률 추적 어려움
- 요청 취소가 복잡함
Axios – 강력한 기능의 대표 주자
Axios가 특별한 이유
Axios는 가장 인기 있는 HTTP 클라이언트 라이브러리로, fetch()의 한계를 극복한 다양한 기능을 제공합니다:
import axios, { AxiosInstance } from 'axios';
// Axios 인스턴스 생성 - 설정 중앙화
const axiosInstance: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000, // 10초 타임아웃 (fetch에는 없는 기능!)
headers: {
'Content-Type': 'application/json',
}
});
// Request Interceptor - 모든 요청 전에 실행
axiosInstance.interceptors.request.use(
async (config) => {
console.log(`🚀 요청: ${config.method?.toUpperCase()} ${config.url}`);
// 자동 토큰 추가
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log('🔐 토큰 추가됨');
}
// 요청 시작 시간 기록 (응답 시간 측정용)
config.metadata = { startTime: Date.now() };
return config;
},
(error) => {
console.error('❌ Request interceptor error:', error);
return Promise.reject(error);
}
);
// Response Interceptor - 모든 응답 후에 실행
axiosInstance.interceptors.response.use(
(response) => {
// 응답 시간 계산
const duration = Date.now() - response.config.metadata.startTime;
console.log(`✅ 응답: ${response.status} (${duration}ms)`);
return response;
},
async (error) => {
const originalRequest = error.config;
// 401 에러 시 토큰 자동 갱신
if (error.response?.status === 401 && !originalRequest._retry) {
console.log('🔄 토큰 만료, 갱신 시도...');
originalRequest._retry = true; // 무한 루프 방지
try {
const refreshToken = localStorage.getItem('refreshToken');
const { data } = await axios.post('/api/auth/refresh', {
refreshToken
});
localStorage.setItem('accessToken', data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
console.log('✅ 토큰 갱신 성공, 요청 재시도');
return axiosInstance(originalRequest);
} catch (refreshError) {
console.error('❌ 토큰 갱신 실패, 로그아웃');
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// 네트워크 에러 처리
if (!error.response) {
console.error('🌐 네트워크 에러:', error.message);
alert('인터넷 연결을 확인해주세요.');
}
return Promise.reject(error);
}
);
Axios의 고급 기능들
// 1. 파일 업로드 with 진행률
export async function uploadFileWithProgress(
file: File,
onProgress?: (percent: number) => void
) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await axiosInstance.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
);
console.log(`📤 업로드 진행률: ${percentCompleted}%`);
onProgress?.(percentCompleted);
}
});
return response.data;
} catch (error) {
if (axios.isCancel(error)) {
console.log('🚫 업로드 취소됨');
}
throw error;
}
}
// 2. 요청 취소
const CancelToken = axios.CancelToken;
let cancelTokenSource = CancelToken.source();
export function cancelableRequest() {
// 이전 요청 취소
cancelTokenSource.cancel('새로운 요청으로 인한 취소');
// 새 토큰 생성
cancelTokenSource = CancelToken.source();
return axiosInstance.get('/data', {
cancelToken: cancelTokenSource.token
});
}
// 3. 동시 요청 처리
export async function fetchMultipleData() {
console.log('🔄 여러 데이터 동시 요청...');
try {
const [users, posts, comments] = await axios.all([
axiosInstance.get('/users'),
axiosInstance.get('/posts'),
axiosInstance.get('/comments')
]);
console.log('✅ 모든 데이터 로드 완료');
return {
users: users.data,
posts: posts.data,
comments: comments.data
};
} catch (error) {
console.error('❌ 동시 요청 실패:', error);
throw error;
}
}
Axios의 장점:
- 자동 JSON 변환
- 요청/응답 인터셉터
- 타임아웃 지원
- 진행률 추적
- 요청 취소
- 브라우저와 Node.js 모두 지원
Axios의 단점:
- 번들 사이즈 (15.4KB)
- 추가 설치 필요
- fetch()보다 약간 느림
Ky – 현대적이고 가벼운 대안
Ky의 철학: fetch()를 더 나은 방식으로
Ky는 fetch() API를 기반으로 하면서도 Axios의 편리한 기능들을 제공하는 현대적인 라이브러리입니다:
import ky from 'ky';
// Ky 인스턴스 생성 - 더 간결한 설정
const kyInstance = ky.create({
prefixUrl: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
retry: {
limit: 2, // 재시도 횟수
methods: ['get', 'put', 'delete'], // 재시도할 메서드
statusCodes: [408, 413, 429, 500, 502, 503, 504], // 재시도할 상태 코드
backoffLimit: 3000 // 최대 대기 시간
},
hooks: {
beforeRequest: [
async (request) => {
console.log(`🚀 Ky 요청: ${request.method} ${request.url}`);
// 토큰 추가
const token = await getAccessToken();
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
// Request ID 추가 (디버깅용)
request.headers.set('X-Request-ID', crypto.randomUUID());
}
],
beforeRetry: [
async ({ request, error, retryCount }) => {
console.log(`🔄 재시도 ${retryCount}회: ${request.url}`);
// 429 (Too Many Requests) 처리
if (error.response?.status === 429) {
const retryAfter = error.response.headers.get('Retry-After');
if (retryAfter) {
const delay = parseInt(retryAfter) * 1000;
console.log(`⏳ ${delay}ms 대기 (Rate Limit)`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
],
afterResponse: [
async (request, options, response) => {
// Rate Limit 정보 로깅
const remaining = response.headers.get('X-RateLimit-Remaining');
if (remaining) {
console.log(`📊 API 호출 잔여 횟수: ${remaining}`);
}
// 응답 시간 로깅
const duration = Date.now() - request.startTime;
console.log(`✅ 응답 시간: ${duration}ms`);
}
]
}
});
// Ky의 간편한 사용법
async function fetchWithKy() {
try {
// GET 요청
const users = await kyInstance.get('users').json();
// POST 요청 - 자동 JSON 직렬화
const newUser = await kyInstance.post('users', {
json: { name: 'John', email: 'john@example.com' }
}).json();
// 검색 파라미터 처리
const filtered = await kyInstance.get('users', {
searchParams: {
role: 'admin',
active: true
}
}).json();
return { users, newUser, filtered };
} catch (error) {
if (error.response) {
const errorBody = await error.response.json();
console.error('API 에러:', errorBody);
}
throw error;
}
}
Ky의 장점:
- fetch() 기반 (표준 준수)
- 작은 번들 사이즈 (9.7KB)
- 자동 재시도
- 간결한 API
- TypeScript 완벽 지원
Ky의 단점:
- 브라우저 전용 (Node.js는 ky-universal 필요)
- Axios보다 커뮤니티 작음
SWR – 데이터 페칭의 새로운 패러다임
SWR이란? Stale-While-Revalidate
SWR은 단순한 HTTP 클라이언트가 아닌, 데이터 페칭 훅입니다. 캐싱, 재검증, 포커스 추적, 리페칭 등을 자동으로 처리합니다:
import useSWR from 'swr';
// 기본 fetcher 함수
const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
};
// SWR 사용 예시
function UserProfile({ userId }: { userId: string }) {
// SWR의 마법! 자동 캐싱, 재검증, 에러 처리
const {
data: user,
error,
isLoading,
isValidating,
mutate
} = useSWR(
userId ? `/api/users/${userId}` : null, // 조건부 페칭
fetcher,
{
revalidateOnFocus: true, // 포커스 시 재검증
revalidateOnReconnect: true, // 재연결 시 재검증
refreshInterval: 0, // 자동 갱신 간격 (0 = 비활성화)
dedupingInterval: 2000, // 중복 요청 제거 간격
onSuccess: (data) => {
console.log('✅ 사용자 데이터 로드:', data);
},
onError: (error) => {
console.error('❌ 로드 실패:', error);
}
}
);
// 로딩 상태
if (isLoading) {
return (
<div className="skeleton">
<div className="skeleton-avatar" />
<div className="skeleton-text" />
</div>
);
}
// 에러 상태
if (error) {
return (
<div className="error">
<p>데이터를 불러올 수 없습니다.</p>
<button onClick={() => mutate()}>다시 시도</button>
</div>
);
}
// 성공 상태
return (
<div className="user-profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
{isValidating && <span className="updating">업데이트 중...</span>}
</div>
);
}
SWR의 고급 패턴들
// 1. 무한 스크롤 구현
function InfiniteUserList() {
const {
data,
error,
size,
setSize,
isValidating,
isLoading
} = useSWRInfinite(
(pageIndex, previousPageData) => {
// 마지막 페이지 도달
if (previousPageData && !previousPageData.length) return null;
// 페이지 URL 생성
return `/api/users?page=${pageIndex}&limit=10`;
},
fetcher
);
const users = data ? data.flat() : [];
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
<button
disabled={isLoadingMore || isReachingEnd}
onClick={() => setSize(size + 1)}
>
{isLoadingMore
? '로딩 중...'
: isReachingEnd
? '더 이상 없습니다'
: '더 보기'}
</button>
</div>
);
}
// 2. 낙관적 업데이트
function TodoList() {
const { data: todos, mutate } = useSWR('/api/todos', fetcher);
const addTodo = async (text: string) => {
const newTodo = {
id: Date.now(), // 임시 ID
text,
completed: false,
optimistic: true // 낙관적 플래그
};
// 낙관적 업데이트 - UI 즉시 반영
mutate(
async (todos) => {
// 서버에 요청
const created = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
}).then(r => r.json());
// 임시 항목을 실제 항목으로 교체
return todos.map(t =>
t.id === newTodo.id ? created : t
);
},
{
optimisticData: [...(todos || []), newTodo],
rollbackOnError: true, // 에러 시 자동 롤백
populateCache: true,
revalidate: false
}
);
};
return (
<div>
{todos?.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
isOptimistic={todo.optimistic}
/>
))}
<AddTodoForm onAdd={addTodo} />
</div>
);
}
// 3. 글로벌 설정
import { SWRConfig } from 'swr';
export function App({ children }) {
return (
<SWRConfig
value={{
fetcher,
revalidateOnFocus: false,
shouldRetryOnError: true,
errorRetryCount: 3,
errorRetryInterval: 1000,
onError: (error, key) => {
console.error(`SWR Error for ${key}:`, error);
if (error.status === 401) {
// 인증 에러 - 로그인 페이지로
window.location.href = '/login';
}
}
}}
>
{children}
</SWRConfig>
);
}
SWR의 장점:
- 자동 캐싱과 재검증
- 포커스/재연결 시 자동 갱신
- 낙관적 업데이트
- 중복 요청 제거
- React Suspense 지원
SWR의 단점:
- React 전용
- 단순 HTTP 요청엔 과도함
TanStack Query – 엔터프라이즈급 솔루션
왜 TanStack Query인가?
TanStack Query(구 React Query)는 가장 강력하고 유연한 데이터 페칭 라이브러리입니다:
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider
} from '@tanstack/react-query';
// QueryClient 설정
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1분간 fresh 상태 유지
gcTime: 5 * 60 * 1000, // 5분간 캐시 유지 (구 cacheTime)
retry: (failureCount, error: any) => {
// 404는 재시도 안 함
if (error.status === 404) return false;
// 최대 3번 재시도
return failureCount < 3;
},
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
mutations: {
onError: (error) => {
console.error('Mutation 에러:', error);
}
}
}
});
// 사용자 데이터 페칭
function UserProfile({ userId }: { userId: string }) {
const {
data: user,
isLoading,
isError,
error,
refetch,
isRefetching
} = useQuery({
queryKey: ['user', userId], // 캐시 키
queryFn: async () => {
console.log(`📡 사용자 ${userId} 정보 페칭...`);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
},
enabled: !!userId, // userId가 있을 때만 실행
// 데이터 변환
select: (data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`
})
});
// Mutation - 사용자 업데이트
const updateUserMutation = useMutation({
mutationFn: async (updates: Partial<User>) => {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!response.ok) throw new Error('Update failed');
return response.json();
},
// 낙관적 업데이트
onMutate: async (updates) => {
// 진행 중인 리페치 취소
await queryClient.cancelQueries({ queryKey: ['user', userId] });
// 이전 값 백업
const previousUser = queryClient.getQueryData(['user', userId]);
// 낙관적 업데이트
queryClient.setQueryData(['user', userId], old => ({
...old,
...updates
}));
return { previousUser };
},
// 에러 시 롤백
onError: (err, updates, context) => {
if (context?.previousUser) {
queryClient.setQueryData(
['user', userId],
context.previousUser
);
}
console.error('업데이트 실패:', err);
},
// 성공 시 재검증
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
}
});
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>에러: {error.message}</div>;
return (
<div>
<h1>{user.fullName}</h1>
<button
onClick={() => refetch()}
disabled={isRefetching}
>
{isRefetching ? '새로고침 중...' : '새로고침'}
</button>
<button
onClick={() => updateUserMutation.mutate({ name: 'New Name' })}
disabled={updateUserMutation.isPending}
>
{updateUserMutation.isPending ? '업데이트 중...' : '이름 변경'}
</button>
</div>
);
}
TanStack Query의 고급 기능
// 1. 의존적 쿼리
function UserWithPosts({ userId }: { userId: string }) {
// 첫 번째 쿼리: 사용자 정보
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// 두 번째 쿼리: 사용자의 포스트 (user 데이터가 있을 때만 실행)
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userQuery.data // 의존성
});
if (userQuery.isLoading) return <div>사용자 로딩 중...</div>;
if (postsQuery.isLoading) return <div>포스트 로딩 중...</div>;
return (
<div>
<h1>{userQuery.data.name}의 포스트</h1>
{postsQuery.data.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// 2. 병렬 쿼리
function Dashboard() {
const queries = useQueries({
queries: [
{
queryKey: ['stats'],
queryFn: fetchStats,
staleTime: 5 * 60 * 1000 // 5분
},
{
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 60 * 1000 // 1분
},
{
queryKey: ['activities'],
queryFn: fetchActivities,
staleTime: 30 * 1000 // 30초
}
]
});
const isLoading = queries.some(q => q.isLoading);
const isError = queries.some(q => q.isError);
if (isLoading) return <div>대시보드 로딩 중...</div>;
if (isError) return <div>일부 데이터 로드 실패</div>;
const [stats, users, activities] = queries.map(q => q.data);
return (
<div>
<StatsWidget data={stats} />
<UsersTable users={users} />
<ActivityFeed activities={activities} />
</div>
);
}
// 3. 무한 쿼리
function InfinitePosts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(
`/api/posts?offset=${pageParam}&limit=20`
);
return response.json();
},
getNextPageParam: (lastPage, pages) => {
// 다음 페이지 파라미터 계산
if (lastPage.length < 20) return undefined;
return pages.length * 20;
},
initialPageParam: 0
});
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>에러 발생</div>;
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? '로딩 중...'
: hasNextPage
? '더 보기'
: '더 이상 없습니다'}
</button>
</div>
);
}
TanStack Query의 장점:
- 가장 강력한 캐싱 시스템
- 의존적/병렬 쿼리
- 무한 스크롤 내장
- DevTools 제공
- SSR/SSG 지원
TanStack Query의 단점:
- 큰 번들 사이즈 (23.4KB)
- 학습 곡선이 가파름
- 단순한 요청엔 과도함
성능 비교와 선택 가이드
번들 사이즈 비교
// 각 라이브러리의 번들 사이즈 영향
const bundleSizes = {
'fetch': {
size: '0 KB',
gzipped: '0 KB',
notes: '브라우저 내장, 추가 다운로드 없음'
},
'axios': {
size: '15.4 KB',
gzipped: '5.9 KB',
notes: '인터셉터, 취소 토큰 등 모든 기능 포함'
},
'ky': {
size: '9.7 KB',
gzipped: '3.3 KB',
notes: 'fetch 기반, 핵심 기능만 포함'
},
'swr': {
size: '12.1 KB',
gzipped: '4.5 KB',
notes: '캐싱, 재검증 로직 포함'
},
'tanstack-query': {
size: '23.4 KB',
gzipped: '8.2 KB',
notes: '전체 기능 포함 (캐싱, 무한 스크롤 등)'
}
};
// 초기 로딩 시간 영향 (3G 네트워크 기준)
// fetch: 0ms
// ky: ~100ms
// swr: ~150ms
// axios: ~200ms
// tanstack-query: ~300ms
선택 가이드: 언제 무엇을 사용할까?
// 1. SSR/SSG 전용 (Next.js Server Components)
// 추천: Native fetch() with Next.js 확장
async function ServerComponent() {
// Next.js의 캐싱과 재검증 활용
const data = await fetch('/api/data', {
next: { revalidate: 3600 }
});
return <div>{/* 서버에서 렌더링 */}</div>;
}
// 2. 간단한 클라이언트 요청
// 추천: fetch() 또는 Ky
function SimpleComponent() {
const handleClick = async () => {
// fetch로 충분
const response = await fetch('/api/action');
const data = await response.json();
// 또는 Ky로 더 간편하게
const data = await ky.post('/api/action').json();
};
}
// 3. 복잡한 상태 관리가 필요한 경우
// 추천: TanStack Query
function ComplexDataComponent() {
const { data, isLoading } = useQuery({
queryKey: ['complex-data'],
queryFn: fetchComplexData,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000
});
}
// 4. 실시간 데이터 동기화
// 추천: SWR
function RealtimeComponent() {
const { data } = useSWR('/api/realtime', fetcher, {
refreshInterval: 1000, // 1초마다 갱신
revalidateOnFocus: true
});
}
// 5. 엔터프라이즈 애플리케이션
// 추천: Axios (인터셉터와 에러 처리가 중요)
const enterpriseAPI = axios.create({
baseURL: process.env.API_URL,
timeout: 30000
});
// 복잡한 인증, 로깅, 에러 처리
enterpriseAPI.interceptors.request.use(authInterceptor);
enterpriseAPI.interceptors.response.use(loggingInterceptor);
의사결정 트리
프로젝트 시작
│
├─ Server-side only? (SSR/SSG)
│ └─ fetch() with Next.js
│
├─ 간단한 요청? (1-2개 엔드포인트)
│ ├─ 번들 크기 중요?
│ │ └─ fetch()
│ └─ 개발 편의성 중요?
│ └─ Ky
│
├─ 캐싱/재검증 필요?
│ ├─ 실시간 동기화 중요?
│ │ └─ SWR
│ └─ 복잡한 쿼리 관리?
│ └─ TanStack Query
│
└─ 엔터프라이즈/레거시?
└─ Axios
마이그레이션 전략
Axios에서 fetch()로 마이그레이션
// 기존 Axios 코드를 fetch로 변환하는 어댑터
class AxiosToFetchAdapter {
private baseURL: string;
constructor(config: { baseURL: string }) {
this.baseURL = config.baseURL;
}
// Axios 스타일 API를 fetch로 구현
async request(config: any) {
const url = this.baseURL + config.url;
const response = await fetch(url, {
method: config.method,
headers: config.headers,
body: config.data ? JSON.stringify(config.data) : undefined
});
if (!response.ok) {
// Axios 스타일 에러 객체
const error: any = new Error(`HTTP ${response.status}`);
error.response = {
status: response.status,
data: await response.json()
};
throw error;
}
return {
data: await response.json(),
status: response.status,
headers: response.headers
};
}
get(url: string, config?: any) {
return this.request({ ...config, method: 'GET', url });
}
post(url: string, data?: any, config?: any) {
return this.request({ ...config, method: 'POST', url, data });
}
}
// 점진적 마이그레이션
const api = process.env.USE_FETCH
? new AxiosToFetchAdapter({ baseURL: 'https://api.example.com' })
: axios.create({ baseURL: 'https://api.example.com' });
// 기존 코드가 그대로 작동
const response = await api.get('/users');
const data = response.data;
결론: 현명한 선택을 위한 체크리스트
HTTP 클라이언트 선택은 프로젝트의 성공에 중요한 영향을 미칩니다. 다음 체크리스트를 통해 최적의 선택을 하세요:
fetch()를 선택하세요:
- Next.js Server Components를 사용 중
- 번들 사이즈가 매우 중요
- 간단한 요청만 필요
- 표준을 중시
Axios를 선택하세요:
- 복잡한 인터셉터 로직 필요
- 파일 업로드 진행률 추적
- 레거시 브라우저 지원
- Node.js와 브라우저 모두 사용
Ky를 선택하세요:
- fetch()의 단점만 보완하고 싶음
- 작은 번들 사이즈 선호
- 현대적인 브라우저만 지원
- TypeScript 프로젝트
SWR을 선택하세요:
- React 프로젝트
- 실시간 데이터 동기화
- 간단한 캐싱 필요
- Vercel 생태계
TanStack Query를 선택하세요:
- 복잡한 데이터 관리
- 무한 스크롤 구현
- 의존적 쿼리 필요
- 강력한 DevTools 필요
기억하세요: 과도한 기능은 복잡성과 번들 사이즈를 증가시킵니다. 프로젝트의 실제 필요에 맞는 도구를 선택하는 것이 가장 중요합니다.