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은 별도로 처리해서 로그인 페이지로 보내거나 세션 만료 안내를 명확히 보여주는 게 맞다.