React 요약 12 - 서버 통신
Updated:
Fetch API
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 기본적인 fetch 사용
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => {
// 응답 상태 확인
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
}
// POST 요청 예제
function CreateUser() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [status, setStatus] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
try {
setStatus('loading');
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, email })
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Created user:', data);
setStatus('success');
// 폼 초기화
setName('');
setEmail('');
} catch (error) {
console.error('Error creating user:', error);
setStatus('error');
}
};
return (
<div>
<h2>Create User</h2>
{status === 'success' && <p className="success">User created successfully!</p>}
{status === 'error' && <p className="error">Failed to create user</p>}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Creating...' : 'Create User'}
</button>
</form>
</div>
);
}
// 커스텀 Hook으로 추상화
function useFetch(url, options = {}) {
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, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
setError(null);
}
} catch (error) {
if (isMounted) {
setError(error.message);
setData(null);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
// 클린업 함수
return () => {
isMounted = false;
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// 커스텀 Hook 사용 예제
function PostList() {
const { data: posts, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/posts?_limit=5'
);
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error loading posts: {error}</div>;
return (
<div>
<h2>Recent Posts</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
코멘트: Fetch API는 브라우저에 내장된 HTTP 요청을 위한 인터페이스입니다. 주요 특징은 다음과 같습니다:
- Promise 기반: 비동기 작업을 처리하기 위한 Promise 반환
- 간단한 인터페이스: 기본적인 HTTP 요청을 쉽게 만들 수 있음
- JSON 처리:
response.json()
을 통한 JSON 응답 처리- 에러 처리: HTTP 상태 코드 확인 및 예외 처리
Fetch API를 사용할 때 주의할 점:
- 네트워크 오류만 reject됨 (HTTP 오류 상태는 수동으로 확인해야 함)
- 기본적으로 쿠키를 보내지 않음 (
credentials: 'include'
옵션 필요)- 타임아웃 설정이 없음 (직접 구현해야 함)
커스텀 Hook으로 추상화하면 코드 재사용성을 높이고 컴포넌트 로직을 단순화할 수 있습니다.
Axios
import { useState, useEffect } from 'react';
import axios from 'axios';
// 기본 설정
axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';
axios.defaults.headers.common['Authorization'] = 'AUTH_TOKEN';
axios.defaults.headers.post['Content-Type'] = 'application/json';
// 인스턴스 생성
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
// 인터셉터 설정
api.interceptors.request.use(
config => {
// 요청 전에 수행할 작업
console.log('Request sent:', config);
return config;
},
error => {
// 요청 오류 처리
console.error('Request error:', error);
return Promise.reject(error);
}
);
api.interceptors.response.use(
response => {
// 응답 데이터 처리
console.log('Response received:', response);
return response;
},
error => {
// 응답 오류 처리
console.error('Response error:', error);
// 401 오류 처리 (인증 실패)
if (error.response && error.response.status === 401) {
console.log('Authentication failed, redirecting to login...');
// 로그인 페이지로 리디렉션 등의 처리
}
return Promise.reject(error);
}
);
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// axios 사용
api.get('/users')
.then(response => {
setUsers(response.data);
setLoading(false);
})
.catch(error => {
setError(error.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
}
// POST 요청 예제
function CreateUser() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [status, setStatus] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
try {
setStatus('loading');
const response = await api.post('/users', { name, email });
console.log('Created user:', response.data);
setStatus('success');
// 폼 초기화
setName('');
setEmail('');
} catch (error) {
console.error('Error creating user:', error);
setStatus('error');
}
};
return (
<div>
<h2>Create User</h2>
{status === 'success' && <p className="success">User created successfully!</p>}
{status === 'error' && <p className="error">Failed to create user</p>}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Creating...' : 'Create User'}
</button>
</form>
</div>
);
}
// 여러 동시 요청
function Dashboard() {
const [data, setData] = useState({ users: [], posts: [], todos: [] });
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchDashboardData = async () => {
try {
setLoading(true);
// 여러 요청 동시에 실행
const [usersResponse, postsResponse, todosResponse] = await Promise.all([
api.get('/users?_limit=5'),
api.get('/posts?_limit=5'),
api.get('/todos?_limit=5')
]);
setData({
users: usersResponse.data,
posts: postsResponse.data,
todos: todosResponse.data
});
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchDashboardData();
}, []);
if (loading) return <div>Loading dashboard data...</div>;
if (error) return <div>Error loading dashboard: {error}</div>;
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="dashboard-section">
<h2>Recent Users</h2>
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
<div className="dashboard-section">
<h2>Recent Posts</h2>
<ul>
{data.posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
<div className="dashboard-section">
<h2>Todo Items</h2>
<ul>
{data.todos.map(todo => (
<li key={todo.id} style=>
{todo.title}
</li>
))}
</ul>
</div>
</div>
);
}
// 커스텀 Hook으로 추상화
function useAxios(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const source = axios.CancelToken.source();
const fetchData = async () => {
try {
setLoading(true);
const response = await api.request({
url,
cancelToken: source.token,
...options
});
if (isMounted) {
setData(response.data);
setError(null);
}
} catch (error) {
if (isMounted && !axios.isCancel(error)) {
setError(error.message);
setData(null);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
// 클린업 함수
return () => {
isMounted = false;
source.cancel('Component unmounted');
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// 커스텀 Hook 사용 예제
function CommentList({ postId }) {
const { data: comments, loading, error } = useAxios(`/posts/${postId}/comments`);
if (loading) return <div>Loading comments...</div>;
if (error) return <div>Error loading comments: {error}</div>;
return (
<div>
<h3>Comments</h3>
{comments.length === 0 ? (
<p>No comments yet.</p>
) : (
<ul>
{comments.map(comment => (
<li key={comment.id}>
<strong>{comment.email}</strong>
<p>{comment.body}</p>
</li>
))}
</ul>
)}
</div>
);
}
코멘트: Axios는 브라우저와 Node.js를 위한 인기 있는 HTTP 클라이언트 라이브러리입니다. Fetch API보다 더 많은 기능을 제공하며, 주요 특징은 다음과 같습니다:
- 자동 JSON 변환: 요청 및 응답 데이터 자동 변환
- 인터셉터: 요청 및 응답을 가로채서 처리
- 요청 취소: 진행 중인 요청 취소 기능
- 타임아웃 설정: 요청 타임아웃 설정 가능
- XSRF 보호: 크로스 사이트 요청 위조 방지
- 에러 처리 개선: HTTP 오류 상태를 자동으로 reject
- 동시 요청:
Promise.all
과 함께 사용하여 여러 요청 동시 처리Axios는 특히 다음과 같은 경우에 유용합니다:
- 복잡한 API 통신이 필요한 경우
- 요청 및 응답 처리를 일관되게 관리해야 하는 경우
- 인증 토큰 관리, 요청 재시도 등의 고급 기능이 필요한 경우
React Query
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from 'react-query';
import axios from 'axios';
// API 함수
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com'
});
const fetchUsers = async () => {
const response = await api.get('/users');
return response.data;
};
const fetchUserById = async (id) => {
const response = await api.get(`/users/${id}`);
return response.data;
};
const createUser = async (userData) => {
const response = await api.post('/users', userData);
return response.data;
};
const updateUser = async ({ id, ...data }) => {
const response = await api.put(`/users/${id}`, data);
return response.data;
};
const deleteUser = async (id) => {
await api.delete(`/users/${id}`);
return id;
};
// QueryClient 설정
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
staleTime: 30000 // 30초
}
}
});
// 앱 컴포넌트
function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="app">
<h1>React Query Example</h1>
<UserList />
</div>
</QueryClientProvider>
);
}
// 사용자 목록 컴포넌트
function UserList() {
const [selectedUserId, setSelectedUserId] = useState(null);
// useQuery로 데이터 가져오기
const { data: users, isLoading, error } = useQuery(
'users', // 쿼리 키
fetchUsers, // 쿼리 함수
{
onError: (error) => {
console.error('Failed to fetch users:', error);
}
}
);
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error loading users: {error.message}</div>;
return (
<div className="user-management">
<div className="user-list">
<h2>Users</h2>
<CreateUserForm />
<ul>
{users.map(user => (
<li key={user.id} onClick={() => setSelectedUserId(user.id)}>
{user.name}
{selectedUserId === user.id && <UserDetail id={user.id} />}
</li>
))}
</ul>
</div>
</div>
);
}
// 사용자 상세 정보 컴포넌트
function UserDetail({ id }) {
const queryClient = useQueryClient();
// 개별 사용자 데이터 쿼리
const { data: user, isLoading, error } = useQuery(
['user', id], // 파라미터가 포함된 쿼리 키
() => fetchUserById(id),
{
enabled: !!id, // id가 있을 때만 쿼리 실행
}
);
// 사용자 삭제 뮤테이션
const deleteMutation = useMutation(
(userId) => deleteUser(userId),
{
onSuccess: (deletedUserId) => {
// 캐시 업데이트
queryClient.setQueryData('users', (oldUsers) =>
oldUsers.filter(user => user.id !== deletedUserId)
);
// 사용자 목록 쿼리 무효화
queryClient.invalidateQueries('users');
}
}
);
// 사용자 업데이트 뮤테이션
const updateMutation = useMutation(
(userData) => updateUser(userData),
{
onSuccess: (updatedUser) => {
// 개별 사용자 캐시 업데이트
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
// 사용자 목록 캐시 업데이트
queryClient.setQueryData('users', (oldUsers) =>
oldUsers.map(user =>
user.id === updatedUser.id ? updatedUser : user
)
);
}
}
);
const handleDelete = () => {
if (window.confirm(`Are you sure you want to delete ${user.name}?`)) {
deleteMutation.mutate(id);
}
};
const handleUpdateName = () => {
const newName = prompt('Enter new name:', user.name);
if (newName && newName !== user.name) {
updateMutation.mutate({ id, name: newName });
}
};
if (isLoading) return <div>Loading user details...</div>;
if (error) return <div>Error loading user: {error.message}</div>;
if (!user) return null;
return (
<div className="user-detail">
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Phone: {user.phone}</p>
<p>Website: {user.website}</p>
<div className="user-actions">
<button onClick={handleUpdateName}>
Update Name
</button>
<button
onClick={handleDelete}
disabled={deleteMutation.isLoading}
>
{deleteMutation.isLoading ? 'Deleting...' : 'Delete'}
</button>
</div>
{deleteMutation.isError && (
<p className="error">Error deleting user: {deleteMutation.error.message}</p>
)}
{updateMutation.isError && (
<p className="error">Error updating user: {updateMutation.error.message}</p>
)}
</div>
);
}
// 사용자 생성 폼 컴포넌트
function CreateUserForm() {
const queryClient = useQueryClient();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// 사용자 생성 뮤테이션
const createMutation = useMutation(
(userData) => createUser(userData),
{
onSuccess: (newUser) => {
// 캐시 업데이트
queryClient.setQueryData('users', (oldUsers) => [...oldUsers, newUser]);
// 입력 필드 초기화
setName('');
setEmail('');
}
}
);
const handleSubmit = (e) => {
e.preventDefault();
if (!name.trim() || !email.trim()) return;
createMutation.mutate({ name, email });
};
return (
<form onSubmit={handleSubmit} className="create-user-form">
<h3>Add New User</h3>
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
</div>
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
</div>
<button
type="submit"
disabled={createMutation.isLoading}
>
{createMutation.isLoading ? 'Adding...' : 'Add User'}
</button>
{createMutation.isError && (
<p className="error">Error creating user: {createMutation.error.message}</p>
)}
{createMutation.isSuccess && (
<p className="success">User created successfully!</p>
)}
</form>
);
}
// 무한 스크롤 예제
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status
} = useInfiniteQuery(
'posts',
async ({ pageParam = 1 }) => {
const response = await api.get('/posts', {
params: { _page: pageParam, _limit: 10 }
});
return response.data;
},
{
getNextPageParam: (lastPage, allPages) => {
// 마지막 페이지가 비어있으면 더 이상 페이지가 없음
return lastPage.length ? allPages.length + 1 : undefined;
}
}
);
// 스크롤 이벤트 처리
const observer = useRef();
const lastPostElementRef = useCallback(node => {
if (isFetchingNextPage) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
}, [isFetchingNextPage, fetchNextPage, hasNextPage]);
if (status === 'loading') return <div>Loading posts...</div>;
if (status === 'error') return <div>Error loading posts</div>;
return (
<div className="post-list">
<h2>Posts</h2>
{data.pages.map((page, i) => (
<React.Fragment key={i}>
{page.map((post, index) => {
// 마지막 항목에 ref 추가
if (page.length === index + 1 && i === data.pages.length - 1) {
return (
<div ref={lastPostElementRef} key={post.id} className="post">
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
);
} else {
return (
<div key={post.id} className="post">
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
);
}
})}
</React.Fragment>
))}
{isFetchingNextPage && <div>Loading more posts...</div>}
</div>
);
}
// 의존적 쿼리 예제
function UserPosts({ userId }) {
// 사용자 정보 쿼리
const { data: user, isLoading: isLoadingUser } = useQuery(
['user', userId],
() => fetchUserById(userId),
{
enabled: !!userId
}
);
// 사용자의 게시물 쿼리 (사용자 정보가 로드된 후에만 실행)
const { data: posts, isLoading: isLoadingPosts } = useQuery(
['userPosts', userId],
async () => {
const response = await api.get(`/users/${userId}/posts`);
return response.data;
},
{
enabled: !!user, // 사용자 데이터가 있을 때만 쿼리 실행
}
);
if (isLoadingUser) return <div>Loading user...</div>;
if (isLoadingPosts) return <div>Loading posts for {user.name}...</div>;
return (
<div className="user-posts">
<h2>Posts by {user.name}</h2>
{posts.length === 0 ? (
<p>No posts found.</p>
) : (
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
)}
</div>
);
}
코멘트: React Query는 서버 상태 관리를 위한 강력한 라이브러리로, 데이터 페칭, 캐싱, 동기화, 업데이트를 쉽게 처리할 수 있게 해줍니다. 주요 특징은 다음과 같습니다:
- 선언적 쿼리:
useQuery
Hook을 통한 데이터 페칭- 자동 캐싱: 쿼리 결과 자동 캐싱 및 재사용
- 백그라운드 업데이트: 오래된 데이터 자동 리페칭
- 뮤테이션:
useMutation
Hook을 통한 데이터 업데이트- 쿼리 무효화: 관련 쿼리 자동 리페칭
- 페이지네이션 및 무한 스크롤: 내장 지원
- 의존적 쿼리: 다른 쿼리 결과에 의존하는 쿼리
- 낙관적 업데이트: 서버 응답 전에 UI 업데이트
React Query는 특히 다음과 같은 경우에 유용합니다:
- 서버 데이터와 클라이언트 상태를 명확히 구분해야 하는 경우
- 복잡한 데이터 페칭 로직이 있는 경우
- 캐싱 및 백그라운드 업데이트가 필요한 경우
- REST API 또는 GraphQL을 사용하는 경우
SWR
import { useState } from 'react';
import useSWR, { useSWRConfig, SWRConfig } from 'swr';
import axios from 'axios';
// API 클라이언트
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com'
});
// 전역 fetcher 함수
const fetcher = async (url) => {
const response = await api.get(url);
return response.data;
};
// SWR 설정으로 앱 래핑
function App() {
return (
<SWRConfig
value=
>
<div className="app">
<h1>SWR Example</h1>
<UserDashboard />
</div>
</SWRConfig>
);
}
// 사용자 대시보드 컴포넌트
function UserDashboard() {
const [userId, setUserId] = useState(1);
return (
<div className="dashboard">
<div className="user-selector">
<label htmlFor="user-select">Select User: </label>
<select
id="user-select"
value={userId}
onChange={(e) => setUserId(Number(e.target.value))}
>
{[1, 2, 3, 4, 5].map(id => (
<option key={id} value={id}>User {id}</option>
))}
</select>
</div>
<div className="dashboard-content">
<UserProfile userId={userId} />
<UserPosts userId={userId} />
<UserTodos userId={userId} />
</div>
</div>
);
}
// 사용자 프로필 컴포넌트
function UserProfile({ userId }) {
const { data: user, error, isValidating } = useSWR(
userId ? `/users/${userId}` : null
);
if (error) return <div className="error">Failed to load user</div>;
if (!user) return <div className="loading">Loading user...</div>;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Phone:</strong> {user.phone}</p>
<p><strong>Website:</strong> {user.website}</p>
<p><strong>Company:</strong> {user.company.name}</p>
{isValidating && <div className="refreshing">Refreshing...</div>}
</div>
);
}
// 사용자 게시물 컴포넌트
function UserPosts({ userId }) {
const { data: posts, error, isValidating } = useSWR(
userId ? `/users/${userId}/posts` : null
);
if (error) return <div className="error">Failed to load posts</div>;
if (!posts) return <div className="loading">Loading posts...</div>;
return (
<div className="user-posts">
<h2>Posts</h2>
{posts.length === 0 ? (
<p>No posts found.</p>
) : (
<ul>
{posts.slice(0, 3).map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}...</p>
</li>
))}
</ul>
)}
{posts.length > 3 && <p>And {posts.length - 3} more posts...</p>}
{isValidating && <div className="refreshing">Refreshing...</div>}
</div>
);
}
// 사용자 할 일 컴포넌트
function UserTodos({ userId }) {
const { data: todos, error, isValidating, mutate } = useSWR(
userId ? `/users/${userId}/todos` : null
);
// 전역 mutate 함수 가져오기
const { mutate: globalMutate } = useSWRConfig();
// 할 일 완료 상태 토글
const toggleTodo = async (todoId, completed) => {
// 현재 todos 데이터 복사
const updatedTodos = todos.map(todo =>
todo.id === todoId ? { ...todo, completed: !completed } : todo
);
// 낙관적 업데이트 (UI 즉시 업데이트)
mutate(updatedTodos, false);
try {
// API 호출
await api.patch(`/todos/${todoId}`, { completed: !completed });
// 성공 시 다시 검증 (서버에서 최신 데이터 가져오기)
mutate();
} catch (error) {
// 실패 시 원래 데이터로 롤백
mutate();
console.error('Failed to update todo:', error);
}
};
if (error) return <div className="error">Failed to load todos</div>;
if (!todos) return <div className="loading">Loading todos...</div>;
// 완료된 할 일과 미완료된 할 일 분리
const completedTodos = todos.filter(todo => todo.completed);
const incompleteTodos = todos.filter(todo => !todo.completed);
return (
<div className="user-todos">
<h2>Todos</h2>
<div className="todo-stats">
<p>Completed: {completedTodos.length}</p>
<p>Pending: {incompleteTodos.length}</p>
<button onClick={() => mutate()}>Refresh</button>
</div>
<h3>Pending Tasks</h3>
{incompleteTodos.length === 0 ? (
<p>No pending tasks!</p>
) : (
<ul className="todo-list">
{incompleteTodos.map(todo => (
<li key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id, todo.completed)}
/>
<span>{todo.title}</span>
</li>
))}
</ul>
)}
<h3>Completed Tasks</h3>
{completedTodos.length === 0 ? (
<p>No completed tasks yet.</p>
) : (
<ul className="todo-list completed">
{completedTodos.map(todo => (
<li key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id, todo.completed)}
/>
<span>{todo.title}</span>
</li>
))}
</ul>
)}
{isValidating && <div className="refreshing">Refreshing...</div>}
</div>
);
}
// 조건부 페칭 예제
function ConditionalFetching() {
const [shouldFetch, setShouldFetch] = useState(false);
// shouldFetch가 true일 때만 데이터 가져오기
const { data, error } = useSWR(shouldFetch ? '/posts' : null);
return (
<div>
<button onClick={() => setShouldFetch(!shouldFetch)}>
{shouldFetch ? 'Stop Fetching' : 'Start Fetching'}
</button>
{shouldFetch && !data && <p>Loading posts...</p>}
{error && <p>Error loading posts</p>}
{data && <p>Loaded {data.length} posts</p>}
</div>
);
}
// 커스텀 훅으로 SWR 추상화
function useUser(id) {
const { data, error, isValidating, mutate } = useSWR(
id ? `/users/${id}` : null
);
return {
user: data,
isLoading: !error && !data,
isError: error,
isValidating,
mutate
};
}
// 페이지네이션 예제
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, error, isValidating } = useSWR(`/posts?_page=${page}&_limit=10`);
const goToPreviousPage = () => {
setPage(p => Math.max(1, p - 1));
};
const goToNextPage = () => {
setPage(p => p + 1);
};
if (error) return <div className="error">Failed to load posts</div>;
if (!data) return <div className="loading">Loading posts...</div>;
return (
<div className="paginated-posts">
<h2>Posts (Page {page})</h2>
<ul className="post-list">
{data.map(post => (
<li key={post.id} className="post-item">
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}...</p>
</li>
))}
</ul>
<div className="pagination-controls">
<button
onClick={goToPreviousPage}
disabled={page === 1 || isValidating}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={goToNextPage}
disabled={data.length < 10 || isValidating}
>
Next
</button>
</div>
{isValidating && <div className="refreshing">Loading...</div>}
</div>
);
}
// 상호 의존적인 데이터 페칭
function PostWithComments() {
const [postId, setPostId] = useState(1);
// 게시물 데이터 가져오기
const { data: post, error: postError } = useSWR(`/posts/${postId}`);
// 게시물 데이터가 있을 때만 댓글 가져오기
const { data: comments, error: commentsError } = useSWR(
() => post ? `/posts/${postId}/comments` : null
);
// 게시물 작성자 정보 가져오기
const { data: user, error: userError } = useSWR(
() => post ? `/users/${post.userId}` : null
);
const isLoading = !post || !comments || !user;
const isError = postError || commentsError || userError;
if (isError) return <div className="error">Failed to load data</div>;
if (isLoading) return <div className="loading">Loading data...</div>;
return (
<div className="post-with-comments">
<div className="post-navigation">
<button
onClick={() => setPostId(id => Math.max(1, id - 1))}
disabled={postId === 1}
>
Previous Post
</button>
<span>Post {postId}</span>
<button onClick={() => setPostId(id => id + 1)}>
Next Post
</button>
</div>
<div className="post-content">
<h2>{post.title}</h2>
<p className="post-author">By: {user.name}</p>
<p>{post.body}</p>
</div>
<div className="post-comments">
<h3>Comments ({comments.length})</h3>
<ul>
{comments.map(comment => (
<li key={comment.id} className="comment">
<p className="comment-email">{comment.email}</p>
<p>{comment.body}</p>
</li>
))}
</ul>
</div>
</div>
);
}
코멘트: SWR(Stale-While-Revalidate)은 Vercel에서 개발한 데이터 페칭 라이브러리로, 캐시된 데이터를 먼저 반환한 후 백그라운드에서 재검증하는 전략을 사용합니다. 주요 특징은 다음과 같습니다:
- 간결한 API:
useSWR
Hook 하나로 대부분의 기능 사용 가능- 자동 재검증: 포커스, 네트워크 재연결, 인터벌 등에 따른 자동 갱신
- 낙관적 UI: 서버 응답 전에 UI 업데이트 가능
- 중복 요청 제거: 동일한 키에 대한 중복 요청 자동 병합
- 페이지네이션 및 무한 스크롤: 내장 지원
- 타입 안전성: TypeScript와 완벽하게 호환
SWR은 특히 다음과 같은 경우에 유용합니다:
- 실시간 데이터가 중요한 대시보드나 모니터링 UI
- 사용자 인터랙션에 따라 자주 변경되는 데이터
- 간단한 API로 강력한 데이터 페칭 기능이 필요한 경우
- 서버 상태와 UI 상태를 명확히 구분하고 싶은 경우
Leave a comment