PromleeBlog
sitemap
aboutMe

posting thumbnail
React API 통신 (Fetch, Axios, GraphQL 전략) - React, 알고 쓰자 11일차
React API Communication (Fetch, Axios, GraphQL) - React Explained Day 11

📅

🚀

들어가기 전에 🔗

우리가 만드는 대부분의 React 애플리케이션은 화면만 보여주는 것을 넘어, 서버와 데이터를 주고받으며 동적으로 정보를 표시하고 사용자와 상호작용합니다.
예를 들어, 최신 뉴스 기사를 불러오거나, 사용자가 작성한 글을 서버에 저장하거나, 상품 목록을 검색하는 등의 작업이 모두 서버와의 통신, 즉
API 통신
을 통해 이루어집니다.
그런데 서버에 데이터를 요청하고 응답을 받는 과정은 즉시 완료되지 않고 시간이 걸리는
비동기(Asynchronous)
작업입니다.
이 비동기 작업을 어떻게 깔끔하게 처리하고, 사용자에게 로딩 상태나 오류 상황을 잘 보여주며, 필요할 때는 요청을 취소하는 등의 관리는 프론트엔드 개발의 중요한 과제 중 하나입니다.
오늘은 React에서 API 통신을 하는 대표적인 방법인 Fetch API와 Axios 라이브러리를 비교해보고, 요청 취소 방법, 그리고 최근 주목받는 GraphQL 통신과 데이터 로딩을 위한 Suspense까지 다양한 전략들을 함께 살펴보겠습니다.

🚀

기본적인 API 통신: Fetch API 🔗

Fetch API는 브라우저에 내장되어 있어 별도의 라이브러리 설치 없이 바로 사용할 수 있는 강력한 비동기 통신 인터페이스입니다.
Promise 기반으로 동작하여 비동기 코드를 비교적 깔끔하게 작성할 수 있습니다.

기본 사용법 (GET 요청) 🔗

가장 기본적인 GET 요청 예시입니다.
API는 https://jsonplaceholder.typicode.com 의 REST API를 사용합니다.
import React, { useState, useEffect } from 'react';
 
interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}
 
function FetchExample() {
  const [post, setPost] = useState<Post | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    setLoading(true);
    setError(null);
 
    fetch('https://jsonplaceholder.typicode.com/posts/1') // API 엔드포인트
      .then(response => {
        // fetch는 네트워크 오류가 아닌 이상 (예: 404, 500) 에러를 throw하지 않음
        // 따라서 응답 상태(ok 또는 status)를 직접 확인해야 함
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        // 응답 본문을 JSON으로 파싱 (이 과정도 Promise 반환)
        return response.json();
      })
      .then(data => {
        setPost(data);
      })
      .catch(err => {
        console.error("Fetch error:", err);
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []); // 마운트 시 한 번만 실행
 
  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>오류 발생: {error.message}</div>;
  if (!post) return null;
 
  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </div>
  );
}

POST 요청 및 옵션 설정 🔗

POST 요청이나 다른 HTTP 메서드, 헤더 설정, 본문(body) 데이터 전송 등은 fetch 함수의 두 번째 인자인 옵션 객체를 통해 설정합니다.
fetch('/api/users', {
  method: 'POST', // HTTP 메서드
  headers: {
    'Content-Type': 'application/json', // 요청 본문 타입
    // 'Authorization': 'Bearer YOUR_TOKEN', // 인증 토큰 등
  },
  body: JSON.stringify({ name: 'Alice', age: 30 }), // JavaScript 객체를 JSON 문자열로 변환
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));

Fetch API의 특징 및 주의점 🔗

➡️

장점 🔗

➡️

단점/주의점 🔗


🚀

Axios 🔗

Axios는 브라우저와 Node.js 환경 모두에서 사용할 수 있는 인기 있는 Promise 기반 HTTP 클라이언트 라이브러리입니다.
Fetch API의 몇 가지 불편한 점들을 개선하고 추가적인 편의 기능을 제공합니다.

기본 사용법 🔗

Axios를 사용하려면 먼저 설치해야 합니다.
npm install axios
  # 또는
yarn add axios
GET 요청 예시입니다.
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // axios 임포트
 
interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}
 
function AxiosExample() {
  const [post, setPost] = useState<Post | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    setLoading(true);
    setError(null);
 
    axios.get<Post>('https://jsonplaceholder.typicode.com/posts/1') // GET 요청 및 응답 타입 지정
      .then(response => {
        // 응답 데이터는 response.data 안에 자동으로 파싱되어 있음
        setPost(response.data);
      })
      .catch(err => {
        // Axios는 4xx, 5xx 응답 상태 코드일 경우 에러(rejected Promise)로 처리함
        console.error("Axios error:", err);
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);
 
  if (loading) return <div>로딩 중...</div>;
  // Axios 에러 객체에서 구체적인 정보 확인 가능 (err.response, err.request 등)
  if (error) return <div>오류 발생: {axios.isAxiosError(error) ? error.message : '알 수 없는 오류'}</div>;
  if (!post) return null;
 
  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </div>
  );
}
POST 요청 예시입니다.
axios.post('/api/users', { name: 'Bob', age: 25 }) // 두 번째 인자로 데이터 객체 전달
  .then(response => console.log('Success:', response.data))
  .catch(error => console.error('Error:', error.response?.data || error.message)); // 에러 응답 확인

Axios의 주요 특징 및 장점 🔗


🚀

Fetch vs Axios 선택 가이드: 언제 무엇을 쓸까? 🔗

Fetch와 Axios는 모두 훌륭한 도구이며, 어떤 것을 선택할지는 프로젝트의 요구사항과 개발자의 선호도에 따라 달라질 수 있습니다.
특징Fetch APIAxios
설치 필요
없음 (브라우저 내장)필요 (라이브러리)
Promise 기반
JSON 변환
수동 (.json() 호출 필요)자동
HTTP 에러 처리
수동 (response.ok 확인 필요)자동 (Promise reject)
요청 취소
AbortController 사용AbortController 또는 CancelToken 사용
인터셉터
없음 (직접 구현 필요)있음 (매우 편리)
타임아웃
없음 (직접 구현 필요)있음 (설정 간편)
호환성
최신 브라우저 위주구형 브라우저 지원 양호
번들 크기
영향 없음라이브러리 크기만큼 증가

🚀

요청 취소하기: AbortController 🔗

사용자가 페이지를 벗어나거나, 새로운 검색어를 입력하여 이전 요청 결과가 더 이상 필요 없어졌을 때 진행 중인 API 요청을 취소하는 것은 불필요한 네트워크 리소스 낭비와 잠재적인 메모리 누수를 막는 데 중요합니다.
이를 위해 표준 기술인 AbortController를 사용할 수 있습니다.

사용 방법 🔗

  1. AbortController 인스턴스를 생성합니다.
  2. 인스턴스의 signal 속성을 fetch나 Axios 요청 옵션에 전달합니다.
  3. 요청을 취소하고 싶을 때, 인스턴스의 abort() 메서드를 호출합니다.
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // Axios 사용 예시
 
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
 
  useEffect(() => {
    if (!query) { // 검색어가 없으면 요청 안 함
      setResults([]);
      return;
    }
 
    // 1. AbortController 생성
    const controller = new AbortController();
 
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get(`/api/search?q=${query}`, {
          // 2. signal 전달
          signal: controller.signal,
        });
        setResults(response.data);
      } catch (error) {
        // 요청 취소 시 Axios는 'CanceledError' 발생시킴
        if (axios.isCancel(error)) {
          console.log('Request canceled:', error.message);
        } else {
          console.error('Search error:', error);
        }
      } finally {
        setLoading(false);
      }
    };
 
    fetchData();
 
    // cleanup 함수: 컴포넌트 언마운트 또는 query 변경 시 이전 요청 취소
    return () => {
      // 3. abort() 호출하여 요청 취소
      console.log(`Canceling request for query: ${query}`);
      controller.abort();
    };
  }, [query]); // query가 변경될 때마다 effect 재실행
 
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="검색어 입력..."
      />
      {loading && <div>검색 중...</div>}
      <ul>
        {results.map((result: any) => (
          <li key={result.id}>{result.name}</li>
        ))}
      </ul>
    </div>
  );
}
useEffect의 cleanup 함수에서 controller.abort()를 호출하여, 컴포넌트가 언마운트되거나 의존성(여기서는 query)이 변경되어 새로운 요청을 보내기 전에 이전 요청을 취소하는 것이 일반적인 패턴입니다.
Fetch API와 함께 사용할 때도 signal 옵션을 동일하게 전달하면 됩니다. 요청이 취소되면 Fetch는 'AbortError'라는 이름의 에러를 발생시킵니다.
AbortController의 동작 방식
AbortController의 동작 방식

🚀

GraphQL 통신과 클라이언트 라이브러리 🔗

앞서 살펴본 Fetch나 Axios는 주로
REST API
와 통신합니다.
REST API는 정해진 엔드포인트를 통해 리소스를 주고받지만, 때로는 필요한 데이터만 효율적으로 가져오기 어려울 수 있습니다(Over/Under-fetching).
GraphQL
은 클라이언트가 필요한 데이터 구조를 직접 쿼리로 요청하고, 서버는 정확히 그 구조에 맞는 데이터만 반환하는 API 쿼리 언어입니다.
이를 통해 네트워크 효율성을 높이고 API 유연성을 확보할 수 있습니다.

GraphQL 클라이언트 (Apollo, urql 등) 🔗

React에서 GraphQL API와 통신할 때는 보통 전용 클라이언트 라이브러리를 사용합니다.
이들은 단순히 요청을 보내는 것을 넘어, GraphQL 통신에 특화된 다양한 편의 기능을 제공합니다.
Apollo Client나 urql 같은 라이브러리는 특히
GraphQL 통신 자체를 원활하게
하고 스키마를 활용하는 데 강점을 가집니다.
// Apollo Client 사용 예시 (설정은 생략)
import React from 'react';
import { useQuery, gql } from '@apollo/client';
 
const GET_USER = gql` /* ... GraphQL 쿼리 ... */ `;
interface UserData { /* ... */ }
interface UserVars { /* ... */ }
 
function UserProfileApollo({ userId }: { userId: string }) {
  const { loading, error, data } = useQuery<UserData, UserVars>(GET_USER, {
    variables: { userId },
  });
  // ... 로딩, 에러, 데이터 처리 ...
}

언제 GraphQL 클라이언트를 고려할까요? 🔗

GraphQL의 데이터 요청/응답 방식
GraphQL의 데이터 요청/응답 방식

🚀

미래의 데이터 로딩: Suspense for Data Fetching 🔗

React 팀은 데이터 로딩 상태(로딩 중, 오류, 성공)를 컴포넌트 내부에서 조건부 렌더링(if (loading) ...)으로 처리하는 대신, React 자체 기능인
Suspense
를 사용하여 더 선언적이고 일관된 방식으로 처리하는 것을 목표로 하고 있습니다.

Suspense 사용 예시 (React Query 활용) 🔗

React Query와 같은 라이브러리는 실험적으로 Suspense 모드를 지원합니다.
useQuery 훅에 suspense: true 옵션을 사용하면, 데이터 로딩 중일 때 Promise를 throw하여 Suspense가 이를 감지하고 fallback을 보여주게 할 수 있습니다.
import React, { Suspense } from 'react'; // Suspense 임포트
import { useQuery, QueryErrorResetBoundary } from 'react-query'; // QueryErrorResetBoundary 추가 (에러 처리용)
import { ErrorBoundary } from 'react-error-boundary'; // 에러 바운더리 라이브러리 예시
import axios from 'axios';
 
interface User { id: string; name: string; email: string; }
const fetchUser = async (userId: string): Promise<User> => {
  const { data } = await axios.get(`/api/users/${userId}`);
  return data;
};
 
// 데이터를 보여주는 실제 컴포넌트
function UserProfileData({ userId }: { userId: string }) {
  const { data } = useQuery<User, Error>(
    ['user', userId],
    () => fetchUser(userId),
    {
      suspense: true, // Suspense 모드 활성화
      // useErrorBoundary: true, // ErrorBoundary와 함께 사용 시 권장
    }
  );
 
  // isLoading, error 상태를 직접 처리할 필요 없음
  return (
    <div>
      <h2>{data.name}</h2>
      <p>Email: {data.email}</p>
    </div>
  );
}
 
// 에러 발생 시 보여줄 컴포넌트
function ErrorFallback({ error, resetErrorBoundary }: any) {
  return (
    <div role="alert">
      <p>문제가 발생했습니다:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </div>
  );
}
 
// Suspense와 ErrorBoundary로 감싸서 사용
function UserProfileSuspense({ userId }: { userId: string }) {
  return (
    // ErrorBoundary로 에러 처리 (Suspense는 로딩 상태만 처리)
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary FallbackComponent={ErrorFallback} onReset={reset}>
          {/* Suspense로 로딩 상태 처리 */}
          <Suspense fallback={<div>사용자 정보 로딩 중...</div>}>
            <UserProfileData userId={userId} />
          </Suspense>
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}
이처럼 Suspense를 사용하면 데이터 로딩 상태 관리가 컴포넌트 외부(Suspense 컴포넌트)로 분리되어, 데이터를 사용하는 컴포넌트는 성공적인 데이터만 다루면 되므로 코드가 더 간결해질 수 있습니다.
에러 처리는 별도의 ErrorBoundary 컴포넌트를 사용하는 것이 일반적입니다.
Suspense for Data Fetching은 아직 발전 중인 기술이지만, 향후 React의 비동기 데이터 처리 방식을 크게 변화시킬 가능성이 있습니다.

🚀

결론 🔗

오늘은 React 애플리케이션에서 서버와 데이터를 주고받는 API 통신과 비동기 처리 전략, 그리고 관련 도구들을 비교 분석했습니다.
효과적인 API 통신과 비동기 처리 전략은 사용자 경험과 애플리케이션 성능에 직접적인 영향을 미칩니다.
오늘 배운 다양한 도구와 기법들을 잘 이해하고 상황에 맞게 활용하여, 더 안정적이고 효율적인 React 애플리케이션을 만들어나가시길 바랍니다.
다음 시간에는 프론트엔드 개발의
보안과 안정성
에 대해 알아보겠습니다. 웹 애플리케이션에서 발생할 수 있는 보안 위협(XSS, CSRF)과 이를 예방하는 방법, 그리고 인증/인가 처리 방식에 대해 이야기 나누겠습니다.

참고 🔗