고급 데이터 가져오기 라이브러리
2025. 4. 1. 00:56ㆍFrameworks/React
고급 데이터 가져오기 라이브러리
기본 fetch나 axios를 사용하는 것 외에도, React 생태계에는 데이터 가져오기를 위한 여러 고급 라이브러리가 있습니다.
1. React Query
React Query는 서버 상태 관리를 위한 라이브러리로, 캐싱, 백그라운드 데이터 업데이트, 중복 요청 제거 등 많은 기능을 제공합니다.
설치:
npm install react-query
사용 예:
import React from 'react';
import { QueryClient, QueryClientProvider, useQuery, useMutation } from 'react-query';
import axios from 'axios';
// QueryClient 생성
const queryClient = new QueryClient();
// 데이터 가져오기 함수
const fetchPosts = async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
return response.data;
};
// 포스트 추가 함수
const addPost = async (newPost) => {
const response = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
return response.data;
};
function PostList() {
// useQuery 훅을 사용한 데이터 가져오기
const { data: posts, isLoading, error } = useQuery('posts', fetchPosts);
// useMutation 훅을 사용한 데이터 생성
const mutation = useMutation(addPost, {
onSuccess: () => {
// 성공 시 'posts' 쿼리 무효화 (데이터 다시 가져오기)
queryClient.invalidateQueries('posts');
},
});
const handleAddPost = () => {
mutation.mutate({
title: '새 포스트',
body: '새 포스트 내용',
userId: 1,
});
};
if (isLoading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error.message}</div>;
return (
<div>
<h1>포스트 목록</h1>
<button onClick={handleAddPost} disabled={mutation.isLoading}>
{mutation.isLoading ? '추가 중...' : '새 포스트 추가'}
</button>
{mutation.isError && <div>에러: {mutation.error.message}</div>}
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
// 애플리케이션에 QueryClientProvider 적용
function App() {
return (
<QueryClientProvider client={queryClient}>
<PostList />
</QueryClientProvider>
);
}
export default App;
2. SWR
SWR(stale-while-revalidate)은 Vercel에서 만든 데이터 가져오기 라이브러리로, 캐싱과 실시간 업데이트 기능을 제공합니다.
설치:
npm install swr
사용 예:
import React from 'react';
import useSWR, { mutate } from 'swr';
import axios from 'axios';
// fetcher 함수
const fetcher = async (url) => {
const response = await axios.get(url);
return response.data;
};
function PostList() {
// useSWR 훅을 사용한 데이터 가져오기
const { data: posts, error, isValidating } = useSWR(
'https://jsonplaceholder.typicode.com/posts',
fetcher,
{ revalidateOnFocus: true } // 페이지 포커스 시 다시 가져오기
);
// 새 포스트 추가
const addPost = async () => {
const newPost = {
title: '새 포스트',
body: '새 포스트 내용',
userId: 1,
};
try {
// API 요청
const response = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
// 데이터 캐시 업데이트 (새 포스트 추가)
mutate('https://jsonplaceholder.typicode.com/posts',
[...(posts || []), response.data],
false // 재검증하지 않음
);
} catch (error) {
console.error('포스트 추가 실패:', error);
}
};
if (error) return <div>에러: {error.message}</div>;
if (!posts) return <div>로딩 중...</div>;
return (
<div>
<h1>포스트 목록</h1>
{isValidating && <div>데이터 업데이트 중...</div>}
<button onClick={addPost}>새 포스트 추가</button>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
export default PostList;
API 인증 처리
많은 API는 인증이 필요합니다. 일반적인 인증 방법은 토큰 기반 인증(JWT 등)입니다.
1. Axios 인스턴스와 인터셉터를 사용한 인증
// services/api.js
import axios from 'axios';
// Axios 인스턴스 생성
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 요청 인터셉터 설정
api.interceptors.request.use(
(config) => {
// 로컬 스토리지에서 토큰 가져오기
const token = localStorage.getItem('token');
// 토큰이 있으면 헤더에 추가
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 응답 인터셉터 설정
api.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
// 401 에러 && 토큰 만료 && 재시도하지 않은 경우
if (error.response.status === 401 && error.response.data.message === 'Token expired' && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 리프레시 토큰으로 새 액세스 토큰 발급
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('https://api.example.com/refresh-token', {
refreshToken
});
// 새 토큰 저장
const { token } = response.data;
localStorage.setItem('token', token);
// 헤더 업데이트
originalRequest.headers.Authorization = `Bearer ${token}`;
// 원래 요청 재시도
return axios(originalRequest);
} catch (refreshError) {
// 리프레시 토큰도 만료된 경우 로그아웃 처리
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
// 로그인 페이지로 리디렉션
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
사용 예:
import React, { useState } from 'react';
import api from '../services/api';
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await api.post('/login', { username, password });
const { token, refreshToken, user } = response.data;
// 토큰 저장
localStorage.setItem('token', token);
localStorage.setItem('refreshToken', refreshToken);
// 사용자 정보 저장 (예: 상태 관리 라이브러리 사용)
// setUser(user);
// 리디렉션
window.location.href = '/dashboard';
} catch (err) {
setError('로그인에 실패했습니다. 사용자명과 비밀번호를 확인하세요.');
}
};
return (
<div>
<h1>로그인</h1>
{error && <div style={{ color: 'red' }}>{error}</div>}
<form onSubmit={handleSubmit}>
<div>
<label>사용자명:</label>
<input
type="text"
value={username}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">로그인</button>
</form>
</div>
);
}
export default Login;
2. 인증 상태 관리를 위한 Context 사용
// contexts/AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import api from '../services/api';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 컴포넌트 마운트 시 로컬 스토리지의 토큰으로 사용자 정보 가져오기
useEffect(() => {
const checkAuth = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const response = await api.get('/me');
setUser(response.data);
} catch (err) {
// 토큰이 유효하지 않은 경우
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
}
}
setLoading(false);
};
checkAuth();
}, []);
// 로그인 함수
const login = async (username, password) => {
try {
setError(null);
const response = await api.post('/login', { username, password });
const { token, refreshToken, user } = response.data;
localStorage.setItem('token', token);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
return true;
} catch (err) {
setError('로그인에 실패했습니다. 사용자명과 비밀번호를 확인하세요.');
return false;
}
};
// 로그아웃 함수
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
setUser(null);
};
// Context 값
const value = {
user,
loading,
error,
login,
logout,
isAuthenticated: !!user
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// 커스텀 훅
export function useAuth() {
return useContext(AuthContext);
}
사용 예:
// App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
// 보호된 라우트 컴포넌트
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>인증 확인 중...</div>;
}
return isAuthenticated ? children : <Navigate to="/login" />;
}
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" />} />
</Routes>
</Router>
</AuthProvider>
);
}
export default App;
// pages/Login.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const { login, error, isAuthenticated } = useAuth();
const navigate = useNavigate();
// 이미 인증된 경우 대시보드로 리디렉션
React.useEffect(() => {
if (isAuthenticated) {
navigate('/dashboard');
}
}, [isAuthenticated, navigate]);
const handleSubmit = async (e) => {
e.preventDefault();
const success = await login(username, password);
if (success) {
navigate('/dashboard');
}
};
return (
<div>
<h1>로그인</h1>
{error && <div style={{ color: 'red' }}>{error}</div>}
<form onSubmit={handleSubmit}>
<div>
<label>사용자명:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label>비밀번호:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">로그인</button>
</form>
</div>
);
}
export default Login;
// pages/Dashboard.js
import React, { useEffect, useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import api from '../services/api';
function Dashboard() {
const { user, logout } = useAuth();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
// 인증이 필요한 API 엔드포인트 호출
const response = await api.get('/dashboard-data');
setData(response.data);
setError(null);
} catch (err) {
setError('데이터를 불러오는데 실패했습니다.');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handleLogout = () => {
logout();
// navigate('/login'); // React Router v6 사용 시
};
return (
<div>
<h1>대시보드</h1>
<p>안녕하세요, {user?.name || 'Guest'}님!</p>
<button onClick={handleLogout}>로그아웃</button>
{loading && <div>데이터 로딩 중...</div>}
{error && <div style={{ color: 'red' }}>{error}</div>}
{data && (
<div>
<h2>데이터</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</div>
);
}
export default Dashboard;
'Frameworks > React' 카테고리의 다른 글
React 컴포넌트 스타일링 방법 (0) | 2025.04.01 |
---|---|
에러 처리와 로딩 상태 관리 (0) | 2025.04.01 |
React 컴포넌트에서 API 연동하기 (0) | 2025.04.01 |
React와 API 연동하기 (0) | 2025.04.01 |
Context API vs Redux (0) | 2025.04.01 |