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 요청을 위한 인터페이스입니다. 주요 특징은 다음과 같습니다:

  1. Promise 기반: 비동기 작업을 처리하기 위한 Promise 반환
  2. 간단한 인터페이스: 기본적인 HTTP 요청을 쉽게 만들 수 있음
  3. JSON 처리: response.json()을 통한 JSON 응답 처리
  4. 에러 처리: 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보다 더 많은 기능을 제공하며, 주요 특징은 다음과 같습니다:

  1. 자동 JSON 변환: 요청 및 응답 데이터 자동 변환
  2. 인터셉터: 요청 및 응답을 가로채서 처리
  3. 요청 취소: 진행 중인 요청 취소 기능
  4. 타임아웃 설정: 요청 타임아웃 설정 가능
  5. XSRF 보호: 크로스 사이트 요청 위조 방지
  6. 에러 처리 개선: HTTP 오류 상태를 자동으로 reject
  7. 동시 요청: 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는 서버 상태 관리를 위한 강력한 라이브러리로, 데이터 페칭, 캐싱, 동기화, 업데이트를 쉽게 처리할 수 있게 해줍니다. 주요 특징은 다음과 같습니다:

  1. 선언적 쿼리: useQuery Hook을 통한 데이터 페칭
  2. 자동 캐싱: 쿼리 결과 자동 캐싱 및 재사용
  3. 백그라운드 업데이트: 오래된 데이터 자동 리페칭
  4. 뮤테이션: useMutation Hook을 통한 데이터 업데이트
  5. 쿼리 무효화: 관련 쿼리 자동 리페칭
  6. 페이지네이션 및 무한 스크롤: 내장 지원
  7. 의존적 쿼리: 다른 쿼리 결과에 의존하는 쿼리
  8. 낙관적 업데이트: 서버 응답 전에 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에서 개발한 데이터 페칭 라이브러리로, 캐시된 데이터를 먼저 반환한 후 백그라운드에서 재검증하는 전략을 사용합니다. 주요 특징은 다음과 같습니다:

  1. 간결한 API: useSWR Hook 하나로 대부분의 기능 사용 가능
  2. 자동 재검증: 포커스, 네트워크 재연결, 인터벌 등에 따른 자동 갱신
  3. 낙관적 UI: 서버 응답 전에 UI 업데이트 가능
  4. 중복 요청 제거: 동일한 키에 대한 중복 요청 자동 병합
  5. 페이지네이션 및 무한 스크롤: 내장 지원
  6. 타입 안전성: TypeScript와 완벽하게 호환

SWR은 특히 다음과 같은 경우에 유용합니다:

  • 실시간 데이터가 중요한 대시보드나 모니터링 UI
  • 사용자 인터랙션에 따라 자주 변경되는 데이터
  • 간단한 API로 강력한 데이터 페칭 기능이 필요한 경우
  • 서버 상태와 UI 상태를 명확히 구분하고 싶은 경우

Categories:

Updated:

Leave a comment