만약 API 응답 시간이 너무 오래 걸린다면 어떻게 처리해야 할까? 사용자가 버튼을 클릭 했는데 아무 반응이 없거나, 무한 로딩만 표시되고 있다면 아마 몇 초도 안돼서 이탈할 것 같다. 이 글에서 React를 사용한 여러 예시를 통해 어떤 방법이 더 좋을지 알아보자.
AbortController로 요청 중단하기
응답 시간이 너무 오래 걸리면 웹 요청을 중단하는 것은 어떨까? "불러오는 중"과 같은 로딩 화면만 보여주는 것이 아닌 시간을 제한하여 사용자를 마냥 기다리게 하지 않고, 이렇게 함으로써 네트워크 트래픽도 줄일 수 있다.
AbortController
는 Fetch API와 같은 웹 요청을 중단 할 수 있게 만들어준다.
AbortController()
생성자를 통해 객체를 생성한 다음, 해당 객체의 signal
속성을 fetch()
메서드의 두 번째 매개변수에 전달한다. 여기서 요청 작업을 취소하려면 객체의 abort()
메서드를 호출하면 된다.
// AbortController 객체 생성
const controller = new AbortController();
const signal = controller.signal;
// fetch 요청
fetch('/endpoint', { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch(() => console.log('에러발생!'));
// 요청 중단 2초 타이머
setTimeout(() => controller.abort(), 2000);
다음으로, 사용자의 정보를 불러오는 간단한 리액트 코드 예시를 통해 알아보자:
import { useState } from 'react';
function App() {
const [users, setUsers] = useState();
const [fetchStatus, setFetchStatus] = useState('idle');
const getUsers = async () => {
setFetchStatus('loading');
const controller = new AbortController();
const signal = controller.signal;
const timeout = setTimeout(() => {
controller.abort();
}, 2000);
try {
const response = await fetch('http://localhost:3000/users', { signal });
const json = await response.json();
setUsers(json);
setFetchStatus('success');
} catch {
setFetchStatus('error');
} finally {
clearTimeout(timeout);
}
};
return (
<>
<button
onClick={getUsers}
disabled={fetchStatus === 'loading'}
type='button'
>
사용자 불러오기
</button>
{fetchStatus === 'success' && <pre>{JSON.stringify(users, null, 2)}</pre>}
{fetchStatus === 'error' && <p>에러 발생!</p>}
</>
);
}
export default App;
유저 정보와 페칭에 대한 상태를 알 수 있는 두 개의 state
변수가 있다. getUsers()
함수로 서버로부터 유저 정보를 불러온다. 여기서 만약 서버에 어떤 문제가 생겼을 경우를 가정하여 응답 시간이 2초 이상이 걸린다면, AbortController
에 의해 요청이 중단되어 예외를 발생시킬 것이다. 이를 위해 json-server를 사용하고, 지연 시간을 임의로 만들어 주었다.
"scripts": {
"start": "vite",
"dev": "json-server db.json --delay 10000"
}
그럼 다음과 같이 동작한다.
요청이 정말 취소가 되었는지 네트워크 탭에서 확인을 해보자.
이렇게 원하는 대로 동작도 하고 문제를 해결한 것 같다. 언뜻 보면 기술적으로 잘 해결한 것 같지만, 여러 상황에 대입해 보면 해당 방법은 최악의 방법이 될 수도 있다.
인터넷 속도가 느린 환경
전세계에서 3G 네트워크와 같은 느린 인터넷 속도를 가진 나라들은 여전히 많다. 오래전 일이라 지금은 빠른지 모르겠지만, 말레이시아에서 와이파이를 연결하고 인터넷을 사용한 적이 있는데, 페이지가 하나하나 그려지는 것을 볼 수 있을 정도였다.
이처럼 인터넷 속도가 느려서 서버와의 통신이 2초 이상 걸리는 경우에는 해당 서비스의 정보에 접근조차 하지 못할 수 있다. 그렇다면 사용자는 해당 서비스를 다신 보고싶지 않을 것이다.
이처럼 결국 작동하게 될 요청을 중단하는 것이 더 나은 경험인지는 더 고민해봐야 할 문제이다. 너무 오래 걸리면 사용자는 페이지를 새로고침 하거나, 인내심을 갖고 조금 더 기다릴 수도 있다.
사용자에게 알려주기
그렇다면 사용자에게 일종의 지연이 발생하고 있음을 알리는건 어떨까? 이렇게 표시하면 어느 정도는 기다려주지 않을까? 통신이 2초 이상 지연된다면 "저기, 평소보다 응답이 오래 걸리네요. 잠시만 기다려 주세요."라고 말해주는 것이다.
import { useState } from 'react';
function App() {
const [users, setUsers] = useState();
const [fetchStatus, setFetchStatus] = useState('idle');
const getUsers = async () => {
setFetchStatus('loading');
const timeout = setTimeout(() => {
setFetchStatus('delayed');
}, 2000);
try {
const response = await fetch('http://localhost:3000/users');
const json = await response.json();
setUsers(json);
setFetchStatus('success');
} catch {
setFetchStatus('error');
} finally {
clearTimeout(timeout);
}
};
return (
<>
<button
onClick={getUsers}
disabled={fetchStatus === 'loading' || fetchStatus === 'delayed'}
type='button'
>
사용자 불러오기
</button>
{fetchStatus === 'success' && <pre>{JSON.stringify(users, null, 2)}</pre>}
{fetchStatus === 'delayed' && (
<p>
응답이 평소보다 느린 것 같습니다. 데이터를 불러오는 데에 다소 시간이
걸릴 수 있습니다. 잠시만 기다려 주세요.
</p>
)}
{fetchStatus === 'error' && <p>에러 발생!</p>}
</>
);
}
export default App;
AbortController
코드들을 모두 지워주고, fetchStatus
에 'delayed'
를 추가해 주었다. 그리고 그에 따른 안내 문구도 작성한다. 그럼 이제 2초 이상 걸리면 사용자는 '조금 느리지만, 여전히 정상 동작하고 있음'은 알 수 있다. 좋다.
이 정도로 만족할 수도 있지만 조금만 더 고민해 보자. 내가 사용자라면 중간에 취소하고 다시 시도를 해보고 싶을 수도 있을 것 같다.
사용자에게 선택권 주기
사용자 경험을 높이기 위한 마지막 방법으로, 직접 선택권을 주는 것은 어떨까? 네트워크 통신이 2초 이상 걸린다면 안내 문구도 표시하면서, 요청을 취소할 수 있는 버튼도 있는 것이다.
import { useRef, useState } from 'react';
function App() {
const [users, setUsers] = useState();
const [fetchStatus, setFetchStatus] = useState('idle');
const controllerRef = useRef();
const getUsers = async () => {
setFetchStatus('loading');
controllerRef.current = new AbortController();
const timeout = setTimeout(() => {
setFetchStatus('delayed');
}, 2000);
try {
const response = await fetch('http://localhost:3000/users', {
signal: controllerRef.current.signal,
});
const json = await response.json();
setUsers(json);
setFetchStatus('success');
} catch (e) {
if (e.name === 'AbortError') {
setFetchStatus('canceled');
} else {
setFetchStatus('error');
}
} finally {
clearTimeout(timeout);
}
};
return (
<>
<button
onClick={getUsers}
disabled={fetchStatus === 'loading' || fetchStatus === 'delayed'}
type='button'
>
사용자 불러오기
</button>
{fetchStatus === 'success' && <pre>{JSON.stringify(users, null, 2)}</pre>}
{fetchStatus === 'delayed' && (
<>
<p>
응답이 평소보다 느린 것 같습니다. 데이터를 불러오는 데에 다소 시간이
걸릴 수 있습니다.
<br />
계속 기다리시거나, 취소할 수 있습니다.
</p>
<button
onClick={() => controllerRef.current.abort()}
className='cancel'
type='button'
>
취소
</button>
</>
)}
{fetchStatus === 'canceled' && <p>취소됨</p>}
{fetchStatus === 'error' && <p>에러 발생!</p>}
</>
);
}
export default App;
요청 취소 기능을 구현하기 위해 AbortController
를 다시 살려보자. 하지만 이번에는 조금 다르게 구현해야 한다. getUsers()
함수에만 이 값을 참조하는 것이 아닌, 취소 버튼 클릭 이벤트에도 해당 값을 참조해야 하기 때문에 useRef를 사용한다.
이제는 사용자가 해당 기능에 대해 약간의 제어도 할 수 있게 되어 조금 더 사용자 친화적인 UI가 되었다.
결론
기술적인 문제에만 집중하다 보면, '응답 시간이 너무 길어. n초 이상 지속되면 요청을 중단하자.'라고 생각할 수 있다. 하지만 이런 점이 어떤 사용자에겐 해당 정보에 접근조차 할 수 없게 만든다.
API 응답 시간이 너무 오래 걸릴 경우에는 사용자가 기다릴 수 있도록 일종의 지연이 발생되는 이유를 설명해 주고, 사용자가 직접 요청을 취소하고 다시 시도할 수 있도록 기능을 제공하는 것이 더 좋은 사용자 경험을 제공한다.
useRef - react.dev
AbortController - MDN
DOMException - MDN
fetch() 전역 함수 - MDN
json-server - typicode
Did Josh Make A Mistake? - Web Dev Simplified
The Most Underrated Data Fetching Trick - Josh tried coding
전체 코드 - GitHub/ByungyeonKim
'React' 카테고리의 다른 글
Vite + React에서 Kakao Maps API 사용하기 (0) | 2024.09.16 |
---|---|
CRA에서 Vite로 마이그레이션: 차세대 툴로 개발 환경 개선하기 (0) | 2024.02.22 |
React 페이지네이션 UI 직접 만들기 (0) | 2023.08.02 |
React에서 Swiper Element 사용 시 TypeScript 에러 해결하기 (0) | 2023.06.18 |
[React] null vs empty fragment (0) | 2023.05.19 |