고급 데이터 가져오기 라이브러리

2025. 4. 1. 00:56Frameworks/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