[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)
  • 잔디 달력

    «   2025/06   »
    일 월 화 수 목 금 토
    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
  • 인기 글

  • 태그

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

티스토리툴바