웹 개발
네트워크 오류로 사용자 입력이 중단될 때
폼 제출 중 네트워크 문제가 생기면 사용자는 로딩 상태에 갇힌다. 타임아웃과 재시도 로직을 미리 준비하자.
사용자가 폼을 제출했는데 네트워크 문제로 요청이 중단된다. 또는 느린 네트워크에서 로딩이 계속 진행되고 있는지 알 수 없다. 이런 경험은 사용자를 혼동시킨다.
첫 번째: 요청에 타임아웃을 설정한다
// fetch API의 AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10초 타임아웃
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData),
signal: controller.signal
});
clearTimeout(timeoutId);
return response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Request timeout');
}
}
네트워크가 느리거나 끊어지면 10초 후 자동으로 요청을 취소한다.
두 번째: 재시도 로직을 추가한다
async function submitWithRetry(formData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData),
signal: AbortSignal.timeout(10000) // 10초 타임아웃
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
if (attempt === maxRetries) {
throw error; // 마지막 시도 실패
}
const delay = Math.pow(2, attempt - 1) * 1000; // 1초, 2초, 4초
console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
}
첫 시도 실패하면 지수 백오프로 다시 시도한다.
세 번째: 사용자에게 상태를 명확히 보여준다
function FormSubmit() {
const [status, setStatus] = useState('idle'); // idle | loading | error | success
const [errorMessage, setErrorMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setStatus('loading');
setErrorMessage('');
try {
const result = await submitWithRetry(formData);
setStatus('success');
} catch (error) {
setStatus('error');
setErrorMessage(
error.message === 'AbortError'
? '요청 시간이 초과되었습니다. 다시 시도해주세요.'
: '제출에 실패했습니다.'
);
}
};
return (
<form onSubmit={handleSubmit}>
<button disabled={status === 'loading'}>
{status === 'loading' ? '제출 중...' : '제출'}
</button>
{status === 'error' && (
<div className="error">
{errorMessage}
<button type="button" onClick={handleSubmit}>
다시 시도
</button>
</div>
)}
{status === 'success' && <div className="success">완료되었습니다!</div>}
</form>
);
}
사용자는 현재 상태를 명확히 알아야 한다. "로딩 중"과 "실패했으니 다시 시도하세요"는 다르다.
네 번째: 느린 네트워크를 감지한다
// Network Information API
if ('connection' in navigator) {
const connection = navigator.connection;
console.log(`Effective type: ${connection.effectiveType}`); // 4g, 3g, 2g, slow-2g
console.log(`Downlink: ${connection.downlink} Mbps`);
console.log(`RTT: ${connection.rtt}ms`);
if (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g') {
// 느린 네트워크 대비
setTimeoutDuration(30000); // 타임아웃을 더 길게
showWarning('느린 네트워크에서 작동 중입니다');
}
}
실제 네트워크 상태를 감지해서 타임아웃을 조절할 수 있다.
다섯 번째: Optimistic Update를 고려한다
const handleSubmit = async (formData) => {
// 즉시 UI를 업데이트 (낙관적 업데이트)
setItems([...items, { ...formData, id: 'temp-id' }]);
try {
const result = await submitWithRetry(formData);
// 서버 응답으로 최종 id 설정
setItems(prev => prev.map(item =>
item.id === 'temp-id' ? { ...item, id: result.id } : item
));
} catch (error) {
// 실패하면 롤백
setItems(prev => prev.filter(item => item.id !== 'temp-id'));
showError('저장에 실패했습니다');
}
};
UI를 먼저 업데이트하고, 백그라운드에서 서버에 제출한다. 네트워크가 느려도 사용자는 즉시 피드백을 받는다.
체크리스트
- 모든 요청에 타임아웃을 설정한다 (최소 10초)
- 타임아웃이나 네트워크 에러 시 재시도 로직을 추가한다
- 사용자에게 "로딩 중", "실패", "성공" 상태를 명확히 보여준다
- Network Information API로 느린 네트워크를 감지한다
- 타임아웃을 너무 짧게 하지 않는다 (느린 네트워크 고려)
- 재시도 횟수를 제한한다 (무한 루프 방지)
- 필요하면 Optimistic Update로 체감 속도를 높인다
네트워크 문제는 피할 수 없다. 하지만 예상하고 대비하면 사용자 경험을 훨씬 개선할 수 있다.