이 글은 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();
}, []);
렌더링 흐름 요약
- 🖼️ 초기 렌더링: isFetching=false, 데이터는 빈 배열
- 🔁 로딩 시작: isFetching=true, 로딩 UI 표시 가능
- ✅ 로딩 완료: 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] 커스텀 훅(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 |
[React] Side Effects / useEffect 훅 / useCallback 훅 (0) | 2025.04.11 |