axios로 바꾸고 에러 처리 다시 짜기

워니의 개발일지·2022년 8월 23일

403인데 아무 일도 일어나지 않는다

1차 개발 때는 REST API 통신을 전부 fetch로 했다. 익숙한 걸로 빠르게 가려는 선택이었다. 이후 axios로 옮겼고, 잘 돌아가는 줄 알았다.

그런데 권한 없는 사용자가 요청을 보냈을 때 아무 일도 일어나지 않았다. 에러 안내도 없고, 화면도 그대로였다. 서버는 403을 정상적으로 내려주고 있었다.

fetch와 axios는 에러를 어디로 보내는가

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 기준으로 짠 분기가 통째로 죽어 있던 이유다.

1. catch는 then 뒤에 둔다

에러가 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 뒤에 둔다.

2. error.response는 항상 있는 게 아니다

순서를 고치고 나서 이렇게 썼다.

// 위험
.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);
  });

결과

  • fetch에서 axios로 옮기는 건 라이브러리 교체가 아니라 에러가 흐르는 경로가 바뀌는 일이었다. fetch는 then, axios는 catch로 간다.
  • catch는 then 뒤에 둔다. 앞에 두면 에러를 처리한 뒤 then이 undefined를 받는다.
  • error.response는 옵셔널 체이닝으로 접근하고, 응답이 아예 없는 실패를 따로 분기한다.

에러 처리 코드가 에러를 내지 않게 하는 것부터가 에러 처리였다.

profile
만들고, 고민하고, 배우며 성장하는 개발 기록

0개의 댓글