← 전체 글로 돌아가기

API

API 요청 실패를 프론트에서 조용히 삼키면 생기는 일

fetch가 에러를 throw하지 않는 경우가 있다. response.ok를 확인하지 않으면 실패한 요청이 성공한 것처럼 처리된다.

fetch()는 네트워크 자체가 끊기지 않는 한 Promise를 reject하지 않는다. 서버가 400이나 500을 응답해도 catch 블록은 실행되지 않는다. 이 사실을 모르고 try/catch만 믿으면 실패한 요청이 조용히 성공으로 처리된다.

fetch의 에러 처리 방식

// 이렇게 쓰면 4xx, 5xx도 '성공'으로 처리된다
try {
  const res = await fetch('/api/posts')
  const data = await res.json() // 에러 응답의 JSON도 그냥 파싱됨
  setPosts(data)
} catch (e) {
  // 여기는 네트워크 오류일 때만 실행된다
  console.error(e)
}

response.ok는 상태 코드가 200-299 범위일 때만 true다. 이걸 확인하지 않으면 서버가 { error: 'Unauthorized' }를 돌려줘도 setPosts({ error: 'Unauthorized' })가 실행된다.

const res = await fetch('/api/posts')
if (!res.ok) {
  throw new Error(`API error: ${res.status}`)
}
const data = await res.json()

사용자가 아무것도 모르는 상황

조용히 실패할 때 진짜 문제는 사용자가 뭔가 잘못됐다는 걸 모른다는 것이다. 폼을 제출했는데 아무 반응이 없거나, 목록이 빈 채로 로딩 스피너만 사라지거나, 이전 데이터를 그대로 보여주는 경우가 여기서 나온다.

최소한 console.error라도 남겨야 개발자 도구에서 원인을 추적할 수 있고, 실제 서비스라면 toast나 에러 상태를 화면에 보여줘야 한다.

async function fetchPosts() {
  setLoading(true)
  setError(null)
  try {
    const res = await fetch('/api/posts')
    if (!res.ok) {
      const body = await res.text()
      throw new Error(`${res.status}: ${body}`)
    }
    const data = await res.json()
    setPosts(data)
  } catch (e) {
    setError('데이터를 불러오지 못했습니다.')
    console.error('[fetchPosts]', e)
  } finally {
    setLoading(false)
  }
}

axios를 쓴다면

axios는 4xx/5xx를 자동으로 throw해줘서 이 문제가 없다. 대신 응답 body가 error.response.data에 있으므로 에러 메시지를 꺼낼 때 error.message만 보면 안 된다.

try {
  const { data } = await axios.get('/api/posts')
  setPosts(data)
} catch (e) {
  if (axios.isAxiosError(e)) {
    console.error(e.response?.data)
  }
  setError('요청에 실패했습니다.')
}

인증 실패는 특히 주의

401이 왔을 때 조용히 처리하면 사용자는 로그인 상태인 줄 알고 계속 쓰다가 나중에 저장이 안 됐다는 걸 뒤늦게 발견한다. 401은 별도로 처리해서 로그인 페이지로 보내거나 세션 만료 안내를 명확히 보여주는 게 맞다.