← 전체 글로 돌아가기

웹 개발

네트워크 오류로 사용자 입력이 중단될 때

폼 제출 중 네트워크 문제가 생기면 사용자는 로딩 상태에 갇힌다. 타임아웃과 재시도 로직을 미리 준비하자.

사용자가 폼을 제출했는데 네트워크 문제로 요청이 중단된다. 또는 느린 네트워크에서 로딩이 계속 진행되고 있는지 알 수 없다. 이런 경험은 사용자를 혼동시킨다.

첫 번째: 요청에 타임아웃을 설정한다

// 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를 먼저 업데이트하고, 백그라운드에서 서버에 제출한다. 네트워크가 느려도 사용자는 즉시 피드백을 받는다.

체크리스트

  1. 모든 요청에 타임아웃을 설정한다 (최소 10초)
  2. 타임아웃이나 네트워크 에러 시 재시도 로직을 추가한다
  3. 사용자에게 "로딩 중", "실패", "성공" 상태를 명확히 보여준다
  4. Network Information API로 느린 네트워크를 감지한다
  5. 타임아웃을 너무 짧게 하지 않는다 (느린 네트워크 고려)
  6. 재시도 횟수를 제한한다 (무한 루프 방지)
  7. 필요하면 Optimistic Update로 체감 속도를 높인다

네트워크 문제는 피할 수 없다. 하지만 예상하고 대비하면 사용자 경험을 훨씬 개선할 수 있다.