1차 개발 때는 REST API 통신을 전부 fetch로 했다. 익숙한 걸로 빠르게 가려는 선택이었다. 이후 axios로 옮겼고, 잘 돌아가는 줄 알았다.
그런데 권한 없는 사용자가 요청을 보냈을 때 아무 일도 일어나지 않았다. 에러 안내도 없고, 화면도 그대로였다. 서버는 403을 정상적으로 내려주고 있었다.
fetch로 짰던 코드는 이랬다.
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ /* ... */ }),
}).then((res) => {
if (res.status === 201) alert('작성되었습니다');
});
fetch는 서버가 403이나 500을 내려줘도 프로미스를 reject하지 않는다. 네트워크 레벨에서 요청 자체가 실패했을 때만 reject한다. 그래서 HTTP 에러는 then 안에서 res.status를 직접 봐야 한다.
이 구조를 그대로 axios로 옮긴 게 문제였다.
axios.post(url, data, { headers }).then((res) => {
if (res.status === 201) alert('작성되었습니다');
});
axios에는 validateStatus 옵션이 있고 기본값이 status >= 200 && status < 300이다. 이 범위를 벗어나면 프로미스를 reject한다. 403은 then에 아예 들어오지 않는다. fetch 기준으로 짠 분기가 통째로 죽어 있던 이유다.
에러가 catch로 간다는 걸 알고 처음엔 이렇게 고쳤다.
// 잘못된 순서
axios
.post(url, data, { headers })
.catch((error) => {
/* 에러 처리 */
})
.then((res) => {
if (res.status === 201) alert('작성되었습니다');
});
에러는 잡혔다. 그런데 catch가 에러를 처리하고 나면 프로미스 체인은 정상 상태로 복구되고, catch가 반환한 값이 다음 then으로 흘러간다. 여기서는 반환값이 없으니 res가 undefined다. res.status를 읽는 순간 TypeError가 한 번 더 난다.
catch 안에서 navigate(-1)로 화면을 되돌리고 있었던 탓에 한동안 눈치채지 못했다. 화면은 이미 넘어간 뒤라 콘솔을 볼 일이 없었다.
catch는 then 뒤에 둔다.
순서를 고치고 나서 이렇게 썼다.
// 위험
.catch((error) => {
const { response } = error;
if (response.status === 403) { /* ... */ }
});
error.response는 서버가 응답을 돌려준 경우에만 존재한다. 타임아웃, 네트워크 끊김, CORS 차단처럼 응답 자체가 없는 실패에서는 undefined다. 그 상태로 response.status를 읽으면 에러 처리 코드가 다시 에러를 낸다. 가장 불안정한 상황에서 화면이 멈추는 셈이다.
최종 코드는 이렇게 정리했다.
axios
.post(url, data, { headers })
.then((res) => {
if (res.status === 201) alert('작성되었습니다');
})
.catch((error) => {
const status = error.response?.status;
if (status === 403) {
alert('권한이 없습니다.');
} else if (status) {
alert('다시 시도해 주세요.');
} else {
alert('네트워크 상태를 확인해 주세요.');
}
navigate(-1);
});
error.response는 옵셔널 체이닝으로 접근하고, 응답이 아예 없는 실패를 따로 분기한다.에러 처리 코드가 에러를 내지 않게 하는 것부터가 에러 처리였다.
