[React] HTTP 통신 : fetch로 데이터 요청하고 로딩/에러 처리까지

2025. 4. 22. 10:19·Programming/React
728x90
반응형

이 글은 Udemy의 【한글자막】 React 완벽 가이드 2025 with React Router & Redux 를 수강하고 정리한 내용입니다.

 

이번 글에서는 React에서 HTTP 요청을 다루는 데 사용되는 기본적인 fetch API와 이를 어떻게 효율적으로 처리할 수 있는지에 대해 알아보겠습니다. 특히, 로딩 상태와 에러 처리를 어떻게 관리하는지를 다룰 예정입니다.

 

✅ 기본 fetch API 사용법 (GET 요청)

1. fetch + .then() 사용

useEffect(() => {
  fetch('<http://localhost:3000/places>')
    .then((response) => response.json())
    .then((resData) => setAvailablePlaces(resData.places));
}, []);

  • response.json()은 비동기 작업이므로 두 번째 .then()에서 실제 데이터를 처리하기 위해 then 체인을 두번 사용하게 됩니다.
  • useEffect에 빈 의존성 배열 ([]) 을 전달해야, 초기 렌더링 시 한 번만 실행됩니다. (무한루프 방지)

 2. async/await 방식

useEffect(() => {
  async function fetchPlaces() {
    const response = await fetch('<http://localhost:3000/places>');
    const resData = await response.json();
    setAvailablePlaces(resData.places);
  }
  fetchPlaces();
}, []);

async/await 방식은 코드가 간결하고 가독성이 좋으며, 에러 처리에도 유리합니다.

 

 

 

 


✅ 로딩 상태 관리

데이터를 요청하는 동안 isFetching 상태를 true로 설정하고, 완료되면 다시 false로 변경합니다.

const [isFetching, setIsFetching] = useState(false);

useEffect(() => {
  async function fetchPlaces() {
    setIsFetching(true); // ✅ 요청 시작 시
    const response = await fetch('<http://localhost:3000/places>');
    const resData = await response.json();
    setAvailablePlaces(resData.places);
    setIsFetching(false); // ✅ 요청 완료 시
  }
  fetchPlaces();
}, []);

렌더링 흐름 요약

  1. 🖼️ 초기 렌더링: isFetching=false, 데이터는 빈 배열
  2. 🔁 로딩 시작: isFetching=true, 로딩 UI 표시 가능
  3. ✅ 로딩 완료: isFetching=false, 데이터 반영
더보기

1. 초기 렌더링 (첫 번째 렌더링)

  • 페이지에 처음 접속하면 useEffect가 실행되기 전에 컴포넌트가 렌더링됩니다.
  • 초기 상태는 isFetching이 false, availablePlaces는 빈 배열입니다.
  • 이때, "Fetching..." 같은 로딩 메시지가 없다면 그냥 빈 UI가 렌더링됩니다.

2. useEffect 실행

  • useEffect가 실행되면 fetchPlaces 함수가 호출되고, **setIsFetching(true)*가 실행됩니다.
  • 이때, isFetching 값이 true로 변경되고, 첫 번째 리렌더링이 발생합니다.
    • 이 리렌더링에서는 isFetching이 true로 변경된 상태이므로, 로딩 UI(예: "Fetching...")가 나타날 수 있습니다.

3. fetch 완료 후 상태 업데이트

  • fetch 요청이 끝나면, 응답을 받아서 setAvailablePlaces(resData.places)와 setIsFetching(false)를 호출합니다.
  • 이때 setAvailablePlaces와 setIsFetching(false)가 두 번 호출됩니다.
    • 두 번째 리렌더링이 발생하고, isFetching이 false로 바뀌며 데이터도 업데이트됩니다.

 

 

 

 


✅ HTTP 에러 처리

  • API 요청 중 실패할 경우를 대비해 try-catch로 에러를 처리합니다.
const [error, setError] = useState();

useEffect(() => {
  async function fetchPlaces() {
    setIsFetching(true);

    try {
      const response = await fetch('<http://localhost:3000/places>');
      const resData = await response.json();

      if (!response.ok) {
        throw new Error('Failed to fetch places.');
      }

      setAvailablePlaces(resData.places);
    } catch (error) {
      setError({ message: error.message || 'Could not fetch places.' });
    }

    setIsFetching(false);
  }

  fetchPlaces();
}, []);

 

 

 

 

 


🐞 위치 정보 수신 전에 로딩이 끝나버리는 문제

문제 상황

위치 기반으로 장소 데이터를 정렬하려는 로직에서, 위치 정보 수신 전에 isFetching을 false로 설정해버려서, 정렬 전 데이터가 화면에 잠깐 노출되는 UX 버그가 발생했습니다.

useEffect(() => {
  async function fetchPlaces() {
    setIsFetching(true);

    try {
      const response = await fetch("<http://localhost:3000/places>");
      const resData = await response.json();

      // ✅ 위치 요청 (비동기)
      navigator.geolocation.getCurrentPosition((position) => {
        const sortedPlaces = sortPlacesByDistance(
          resData.places,
          position.coords.latitude,
          position.coords.longitude
        );
        setAvailablePlaces(sortedPlaces);
      });
    } catch (error) {
      setError({ message: error.message });
    }

    setIsFetching(false); // ❗ 위치 정렬이 끝나기 전에 로딩 상태 종료됨
  }

  fetchPlaces();
}, []);

실제 콘솔 로그 흐름 예시

[1] === 📡 [fetchPlaces] 시작 ===
[2] 🔄 [isFetching] true 설정됨
[3] 📥 [fetch] 요청 성공
[4] 📦 [JSON 파싱 완료] {places: Array(18)}
[5] 📍 [위치 요청] 사용자 위치 가져오는 중...
[6] ✅ [isFetching] false 설정됨    // <=== 💥 위치 기반 정렬은 아직 안 끝났음
[7] === 📡 [fetchPlaces] 종료 ===
[8] ✅ [위치 정보] 수신 완료: latitude: 37.4111, longitude: 127.0983
[9] 🗂️ [정렬 완료] 거리순 장소 목록: (18) [...]
[10] 📌 [setAvailablePlaces] 정렬된 데이터 적용

문제의 원인

포인트 설명 설명
⚠️ getCurrentPosition()은 비동기 위치 정보는 즉시 반환되지 않고, 나중에 콜백이 실행됩니다.
⚠️ setIsFetching(false)가 너무 일찍 실행됨 위치 정보가 도착하기도 전에 로딩을 종료시켜 버립니다.
⚠️ 사용자 입장에선 이상하게 느껴짐 처음엔 기본 정렬 데이터가 보이다가, 몇 초 후에 갑자기 재정렬되어 "깜빡이거나 버벅이는 UI" 가 됩니다.

✅ 해결 방법

이 문제를 해결하기 위해서는 위치 정보 요청과 로딩 상태 변경을 적절히 관리해야 합니다. setIsFetching(false)를 위치 정보 수신 후에 변경되도록 수정하여, 데이터가 제대로 정렬된 후에 로딩 상태가 종료되도록 합니다.

수정된 코드

useEffect(() => {
  async function fetchPlaces() {
    setIsFetching(true);

    try {
      const response = await fetch("<http://localhost:3000/places>");
      const resData = await response.json();

      navigator.geolocation.getCurrentPosition((position) => {
        const sortedPlaces = sortPlacesByDistance(
          resData.places,
          position.coords.latitude,
          position.coords.longitude
        );
        setAvailablePlaces(sortedPlaces);
        setIsFetching(false); // ✅ 위치 기반 정렬이 끝난 후 로딩 종료
      });
    } catch (error) {
      setError({ message: error.message });
      setIsFetching(false); // ⚠️ 에러가 난 경우도 잊지 말고 로딩 종료
    }
  }

  fetchPlaces();
}, []);

변경 후 로그 흐름 예시

[1] === 📡 [fetchPlaces] 시작 ===  
[2] 🔄 [isFetching] true 설정됨  
[3] 📥 [fetch] 요청 성공  
[4] 📦 [JSON 파싱 완료] {places: Array(18)}  
[5] 📍 [위치 요청] 사용자 위치 가져오는 중...  
[6] === 📡 [fetchPlaces] 종료 ===  
[7] ✅ [위치 정보] 수신 완료: latitude: 37.4111713, longitude: 127.098369  
[8] 🗂️ [정렬 완료] 거리순 장소 목록: (18) [...]  
[9] 📌 [setAvailablePlaces] 정렬된 데이터 적용  
[10] ✅ [isFetching] false 설정됨  

이제 setIsFetching(false)가 위치 정보를 받고, 장소 목록을 정렬한 후 호출되므로, 로딩 UI가 올바르게 처리됩니다.

 

 

 


✅ 기본 fetch API 사용법 (PUT 요청)

https.js

export async function updateUserPlaces(places) {
  const response = await fetch("<http://localhost:3000/user-places>", {
    method: "PUT",
    body: JSON.stringify({ places }), // {places: places}와 동일
    headers: {
      "Content-Type": "application/json",
    },
  });

  const resData = await response.json();

  if (!response.ok) {
    throw new Error("Failed to update user places.");
  }

  return resData.message;
}

App.jsx

export async function handleSelectPlace(selectedPlace) {
		// 상태 업데이트
    setUserPlaces((prevPickedPlaces) => {
      ....
      return [selectedPlace, ...prevPickedPlaces];
    });

    // backend 통신 함수 호출
    try {
      await updateUserPlaces([selectedPlace, ...userPlaces]);
    } catch (error) {
      console.error("Error updating user places:", error);
      setUserPlaces(userPlaces); // 백엔드 단에서 에러가 발생했을 때, 원래의 userPlaces로 되돌리기
    }
  }

 

 

 

 


✅ 낙관적 업데이트 vs 비관적 업데이트

낙관적 업데이트 (Optimistic Update)

“낙관적 업데이트(Optimistic Update)”는 UI를 더 빠르게 반응시키기 위한 전략 중 하나로 서버의 응답을 기다리지 않고, 사용자에게 먼저 결과가 적용된 것처럼 보여주는 방식 말합니다.

사용자 행동에 즉시 반응하여 UI를 먼저 업데이트한 후, 백엔드 응답 결과에 따라 유지하거나 롤백합니다.

async function handleSelectPlace(selectedPlace) {
  setUserPlaces((prevPickedPlaces) => [selectedPlace, ...prevPickedPlaces]);

  try {
    await updateUserPlaces([selectedPlace, ...userPlaces]);
  } catch (error) {
    console.error("Error updating user places:", error);
    setUserPlaces(userPlaces); // ❗ 실패 시 원래 상태로 되돌리기
  }
}

✅ 장점

  • 체감 성능이 빠르고 반응성이 좋음
  • UX가 즉각적이고 부드러움

❌ 단점

  • 실패 가능성을 고려해야 하며
  • 되돌리기(rollback) 로직을 반드시 구현해야 함

 

비관적 업데이트 (Pessimistic Update)

서버의 응답을 받은 후에야 UI를 업데이트합니다.

async function handleSelectPlace(selectedPlace) {
  await updateUserPlaces([selectedPlace]);

  setUserPlaces((prevPickedPlaces) => {
    if (prevPickedPlaces.some((place) => place.id === selectedPlace.id)) {
      return prevPickedPlaces;
    }
    return [selectedPlace, ...prevPickedPlaces];
  });
}

✅ 특징

  • 안정성은 높지만
  • 사용자 입장에서 반응이 느려 보일 수 있음

 

 

 

 


도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!

궁금한 점이 있다면 언제든지 질문 환영입니다 😄

지금까지 읽어주셔서 감사합니다!

반응형

'Programming > React' 카테고리의 다른 글

[React] style Prop 사용하여 Inline(인라인)으로 css 적용하기  (0) 2025.04.23
[React] 커스텀 훅(Custom Hook)  (0) 2025.04.22
[React] 리액트 클래스 컴포넌트, 왜 아직도 알아야 할까?  (1) 2025.04.18
[React]⚡️React 성능 최적화 정리: memo, useMemo, 그리고 key의 역할까지!  (0) 2025.04.17
[React] React로 퀴즈 앱 만들기 | 개발 회고  (1) 2025.04.16
'Programming/React' 카테고리의 다른 글
  • [React] style Prop 사용하여 Inline(인라인)으로 css 적용하기
  • [React] 커스텀 훅(Custom Hook)
  • [React] 리액트 클래스 컴포넌트, 왜 아직도 알아야 할까?
  • [React]⚡️React 성능 최적화 정리: memo, useMemo, 그리고 key의 역할까지!
제가안난데여♪(´ε`*)
제가안난데여♪(´ε`*)
기억의 유한함을 기록의 무한함으로 ✍️ 예비 개발자가 꿈꾸는 공간 여기는 안나의 개발 블로그 💻
  • 제가안난데여♪(´ε`*)
    안나의 전두엽 어딘가 🧠
    제가안난데여♪(´ε`*)
    기억의 유한함을 기록의 무한함으로 ✍️ 예비 개발자가 꿈꾸는 공간 여기는 안나의 개발 블로그 💻
  • 전체
    오늘
    어제
    • 분류 전체보기 (128)
      • 간단하게 한스푼🥄 (1)
      • Programming (56)
        • Spring (16)
        • Vue.js (6)
        • Deep Learning (3)
        • DataBase (5)
        • React (26)
      • DevOps (21)
        • Docker (12)
        • Linux (4)
      • Algorithm (46)
        • 알고리즘 정리 (5)
        • 자료구조 (0)
        • PS - 백준 (28)
        • 99클럽 코테 스터디 (13)
      • Project (0)
        • CampFire (0)
      • 안나의 취뽀일기 (˵ •̀ ᴗ - ˵ ) ✧ (4)
        • SSAFY_9기 (2)
        • SW 부트캠프 (2)
  • 잔디 달력

    «   2026/01   »
    일 월 화 수 목 금 토
    1 2 3
    4 5 6 7 8 9 10
    11 12 13 14 15 16 17
    18 19 20 21 22 23 24
    25 26 27 28 29 30 31
  • 인기 글

  • 태그

    김영한
    Vue.js 입문하기
    front-end
    항해99
    도커컨테이너
    docker
    도커
    java
    리액트 상태
    99클럽
    java stack
    stack
    Spring
    백준 java
    자바
    자바 스택
    알고리즘
    Vue.js
    백준 구현문제
    백준
    topology sort
    Vue
    greedy
    java 백준
    til
    springboot
    React
    코딩테스트 준비
    개발자 취업
    linux
  • 01-24 06:09
    반응형
  • hELLO· Designed By정상우.v4.10.3
제가안난데여♪(´ε`*)
[React] HTTP 통신 : fetch로 데이터 요청하고 로딩/에러 처리까지
상단으로

티스토리툴바