React에서 비동기 데이터 요청: fetch와 axios 비교 가이드

odada·2025년 3월 5일
0

React에서 비동기 데이터 요청: fetch와 axios 비교 가이드

React 애플리케이션에서 API와 통신하기 위한 비동기 데이터 요청은 필수적인 기능입니다. 이 문서에서는 JavaScript 내장 API인 fetch와 인기 있는 HTTP 클라이언트 라이브러리인 axios의 문법과 사용법을 비교하고 정리합니다.

목차

기본 설정

Fetch API

Fetch는 모던 브라우저에 내장된 API로 별도의 설치가 필요 없습니다.

Axios

Axios는 외부 라이브러리이므로 설치가 필요합니다:

# npm 사용
npm install axios

# yarn 사용
yarn add axios

React 프로젝트에서 가져오기:

// Fetch는 별도의 import 필요 없음

// Axios import
import axios from 'axios';

Fetch API

Fetch 기본 문법

기본적인 GET 요청:

fetch('https://api.example.com/data')
  .then(response => {
    // response.ok는 HTTP 상태 코드가 200-299 범위인 경우 true
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json(); // JSON으로 파싱
  })
  .then(data => {
    console.log(data); // 데이터 사용
  })
  .catch(error => {
    console.error('Fetch error:', error);
  });

async/await 구문 사용:

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('Fetch error:', error);
  }
};

Fetch HTTP 메서드

다양한 HTTP 메서드 사용:

// POST 요청
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com'
  })
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// PUT 요청
fetch('https://api.example.com/data/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Updated Name',
    email: 'updated@example.com'
  })
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// DELETE 요청
fetch('https://api.example.com/data/1', {
  method: 'DELETE'
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

Fetch 에러 처리

Fetch는 네트워크 오류만 reject하고, HTTP 오류 상태는 reject하지 않습니다:

fetch('https://api.example.com/nonexistent')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => {
    console.error('Error:', error);
    // 여기서 네트워크 오류나 위에서 throw한 오류를 처리
  });

Fetch 헤더 설정

헤더 설정:

fetch('https://api.example.com/data', {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_TOKEN',
    'Accept': 'application/json'
  }
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

Fetch 요청 취소

AbortController를 사용한 요청 취소:

const controller = new AbortController();
const signal = controller.signal;

// 요청 시작
fetch('https://api.example.com/data', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  });

// 5초 후 요청 취소
setTimeout(() => {
  controller.abort();
  console.log('Fetch aborted after 5 seconds');
}, 5000);

Fetch React Hooks와 함께 사용

useEffect와 함께 사용하는 패턴:

import { useState, useEffect } from 'react';

function DataFetchingComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://api.example.com/data', { signal });
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setData(null);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 클린업 함수
    return () => {
      controller.abort();
    };
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!data) return <div>No data</div>;

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Axios

Axios 기본 문법

기본적인 GET 요청:

axios.get('https://api.example.com/data')
  .then(response => {
    // 응답 데이터는 response.data에 있음
    console.log(response.data);
  })
  .catch(error => {
    console.error('Axios error:', error);
  });

async/await 구문 사용:

const fetchData = async () => {
  try {
    const response = await axios.get('https://api.example.com/data');
    console.log(response.data);
    return response.data;
  } catch (error) {
    console.error('Axios error:', error);
  }
};

Axios HTTP 메서드

다양한 HTTP 메서드 사용:

// POST 요청
axios.post('https://api.example.com/data', {
  name: 'John Doe',
  email: 'john@example.com'
})
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));

// PUT 요청
axios.put('https://api.example.com/data/1', {
  name: 'Updated Name',
  email: 'updated@example.com'
})
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));

// DELETE 요청
axios.delete('https://api.example.com/data/1')
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));

// 여러 메서드를 method 옵션을 통해 사용
axios({
  method: 'post',
  url: 'https://api.example.com/data',
  data: {
    name: 'John Doe',
    email: 'john@example.com'
  }
})
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));

Axios 에러 처리

Axios는 HTTP 오류 상태를 자동으로 reject합니다:

axios.get('https://api.example.com/nonexistent')
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    if (error.response) {
      // 서버가 2xx 범위를 벗어난 상태 코드로 응답
      console.error('Error status:', error.response.status);
      console.error('Error data:', error.response.data);
    } else if (error.request) {
      // 요청이 이루어졌지만 응답을 받지 못함
      console.error('Error request:', error.request);
    } else {
      // 오류를 발생시킨 요청을 설정하는 중에 문제 발생
      console.error('Error message:', error.message);
    }
    console.error('Error config:', error.config);
  });

Axios 헤더 설정

헤더 설정:

// 요청별 헤더 설정
axios.get('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer YOUR_TOKEN',
    'Accept': 'application/json'
  }
})
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));

// 전역 기본 헤더 설정
axios.defaults.headers.common['Authorization'] = 'Bearer YOUR_TOKEN';
axios.defaults.headers.post['Content-Type'] = 'application/json';

Axios 요청 취소

CancelToken을 사용한 요청 취소 (v0.22.0 이하):

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 요청 시작
axios.get('https://api.example.com/data', {
  cancelToken: source.token
})
  .then(response => console.log(response.data))
  .catch(error => {
    if (axios.isCancel(error)) {
      console.log('Request canceled:', error.message);
    } else {
      console.error('Error:', error);
    }
  });

// 5초 후 요청 취소
setTimeout(() => {
  source.cancel('Operation canceled by the user.');
}, 5000);

AbortController를 사용한 요청 취소 (v0.22.0 이상):

const controller = new AbortController();

// 요청 시작
axios.get('https://api.example.com/data', {
  signal: controller.signal
})
  .then(response => console.log(response.data))
  .catch(error => {
    if (error.name === 'CanceledError') {
      console.log('Request canceled');
    } else {
      console.error('Error:', error);
    }
  });

// 5초 후 요청 취소
setTimeout(() => {
  controller.abort();
}, 5000);

Axios 인터셉터

요청 및 응답 인터셉터:

// 요청 인터셉터 추가
axios.interceptors.request.use(
  config => {
    // 요청 보내기 전에 수행할 작업
    console.log('Request sent:', config.url);
    // 인증 토큰 추가
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
    return config;
  },
  error => {
    // 요청 오류 처리
    return Promise.reject(error);
  }
);

// 응답 인터셉터 추가
axios.interceptors.response.use(
  response => {
    // 응답 데이터를 처리
    console.log('Response received:', response.status);
    return response;
  },
  error => {
    // 응답 오류 처리
    if (error.response && error.response.status === 401) {
      // 인증 오류 처리 (예: 로그아웃 또는 토큰 갱신)
      console.log('Authentication error');
    }
    return Promise.reject(error);
  }
);

Axios 인스턴스 생성

사용자 정의 설정으로 인스턴스 생성:

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  }
});

// 인스턴스 사용
api.get('/data')
  .then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));

Axios React Hooks와 함께 사용

useEffect와 함께 사용하는 패턴:

import { useState, useEffect } from 'react';
import axios from 'axios';

function DataFetchingComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await axios.get('https://api.example.com/data', {
          signal: controller.signal
        });
        setData(response.data);
        setError(null);
      } catch (err) {
        if (err.name !== 'CanceledError') {
          setError(err.message);
          setData(null);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 클린업 함수
    return () => {
      controller.abort();
    };
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!data) return <div>No data</div>;

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Fetch와 Axios 비교

기능FetchAxios
브라우저 지원최신 브라우저 (IE 불가)모든 브라우저
설치 필요아니요 (내장 API)예 (외부 라이브러리)
JSON 자동 변환수동 파싱 필요자동 변환
요청 취소AbortController 사용CancelToken 또는 AbortController 사용
타임아웃 설정직접 구현 필요내장 지원
인터셉터지원 안 함지원
에러 처리HTTP 오류를 reject하지 않음HTTP 오류를 자동으로 reject
업로드 진행 모니터링지원 제한적내장 지원
CSRF 보호수동 구현 필요일부 내장 지원
응답 타임아웃수동 구현 필요내장 지원

실제 사용 예제

사용자 인증 (로그인) 예제

Fetch 사용:

import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch('https://api.example.com/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });
      
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || '로그인에 실패했습니다.');
      }
      
      const data = await response.json();
      // 로그인 성공 처리
      localStorage.setItem('token', data.token);
      window.location.href = '/dashboard';
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      <div>
        <label>이메일:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label>비밀번호:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      <button type="submit" disabled={loading}>
        {loading ? '로그인 중...' : '로그인'}
      </button>
    </form>
  );
}

Axios 사용:

import { useState } from 'react';
import axios from 'axios';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    
    try {
      const response = await axios.post('https://api.example.com/login', {
        email,
        password
      });
      
      // 로그인 성공 처리 (Axios에서 data 프로퍼티에 바로 접근)
      localStorage.setItem('token', response.data.token);
      window.location.href = '/dashboard';
    } catch (err) {
      setError(
        err.response?.data?.message || 
        '로그인에 실패했습니다.'
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      <div>
        <label>이메일:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label>비밀번호:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      <button type="submit" disabled={loading}>
        {loading ? '로그인 중...' : '로그인'}
      </button>
    </form>
  );
}

데이터 CRUD 예제 (커스텀 훅)

Fetch 사용 커스텀 훅:

import { useState, useEffect, useCallback } from 'react';

export function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchData = useCallback(async (customOptions = {}) => {
    const controller = new AbortController();
    const signal = controller.signal;
    
    setLoading(true);
    
    try {
      const response = await fetch(url, {
        ...options,
        ...customOptions,
        signal,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
          ...customOptions.headers,
        },
      });
      
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.message || `HTTP error ${response.status}`);
      }
      
      const result = await response.json();
      setData(result);
      setError(null);
      return result;
    } catch (err) {
      if (err.name !== 'AbortError') {
        setError(err.message);
        throw err;
      }
    } finally {
      setLoading(false);
    }
    
    return () => controller.abort();
  }, [url, options]);
  
  useEffect(() => {
    if (!options.manual) {
      const controller = new AbortController();
      const signal = controller.signal;
      
      setLoading(true);
      
      fetch(url, { ...options, signal })
        .then(response => {
          if (!response.ok) {
            return response.json().catch(() => ({}))
              .then(errorData => {
                throw new Error(errorData.message || `HTTP error ${response.status}`);
              });
          }
          return response.json();
        })
        .then(data => {
          setData(data);
          setError(null);
        })
        .catch(err => {
          if (err.name !== 'AbortError') {
            setError(err.message);
          }
        })
        .finally(() => {
          setLoading(false);
        });
      
      return () => controller.abort();
    }
  }, [url, options]);
  
  return { data, loading, error, fetchData };
}

Axios 사용 커스텀 훅:

import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

export function useAxios(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const source = axios.CancelToken.source();
  
  const fetchData = useCallback(async (customOptions = {}) => {
    const controller = new AbortController();
    
    setLoading(true);
    
    try {
      const response = await axios({
        url,
        cancelToken: source.token,
        signal: controller.signal,
        ...options,
        ...customOptions,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
          ...customOptions.headers,
        },
      });
      
      setData(response.data);
      setError(null);
      return response.data;
    } catch (err) {
      if (!axios.isCancel(err)) {
        setError(err.response?.data?.message || err.message);
        throw err;
      }
    } finally {
      setLoading(false);
    }
    
    return () => {
      source.cancel('Operation canceled by cleanup.');
      controller.abort();
    };
  }, [url, options, source]);
  
  useEffect(() => {
    if (!options.manual) {
      setLoading(true);
      
      const controller = new AbortController();
      
      axios({
        url,
        cancelToken: source.token,
        signal: controller.signal,
        ...options,
      })
        .then(response => {
          setData(response.data);
          setError(null);
        })
        .catch(err => {
          if (!axios.isCancel(err)) {
            setError(err.response?.data?.message || err.message);
          }
        })
        .finally(() => {
          setLoading(false);
        });
      
      return () => {
        source.cancel('Operation canceled by cleanup.');
        controller.abort();
      };
    }
    
    return () => {
      source.cancel('Operation canceled by cleanup.');
    };
  }, [url, options, source]);
  
  return { data, loading, error, fetchData };
}

결론

fetchaxios 모두 React 애플리케이션에서 비동기 HTTP 요청을 처리하는 훌륭한 도구입니다.

  • Fetch는 별도의 설치 없이 바로 사용할 수 있고, 표준 Web API로 브라우저와의 호환성이 향상되고 있습니다.
  • Axios는 더 많은 기능과 사용하기 쉬운 API를 제공하며, 인터셉터, 요청 취소, 자동 JSON 변환 등 유용한 기능을 내장하고 있습니다.

프로젝트 요구사항, 팀의 선호도, 필요한 기능에 따라 적절한 도구를 선택하시기 바랍니다. 작은 프로젝트나 빠른 프로토타이핑에는 fetch가 충분할 수 있으며, 복잡한 프로젝트나 엔터프라이즈 애플리케이션에는 axios의 추가 기능과 확장성이 도움이 될 수 있습니다.

0개의 댓글