비동기 요청 에러 핸들링하기 : Root Return

nnm·2020년 3월 31일
0

프로그래머스 2020 Dev-Matching : 웹 프론트엔드 과제 복기
https://github.com/woohyeonjo/ilovecat

에러를 잘 처리하는 것은 사용자에겐 향상된 UX를 제공하고 개발자에겐 에러에 대한 확실한 인지와 개선을 할 수 있도록 한다. 그렇다면 좋은 에러 처리란 무엇일까? 검색을 하다가 자바스크립트 에러 핸들링 : 신뢰 할 만한 가이드라는 글을 읽게되었고 그를 바탕으로 프로젝트에 에러 핸들링을 해보았다. 하지만 여전히 좋은 에러 처리가 무엇인지는 잘 모르겠다. 많이 해보고 겪어봐야 느낌이 올 것 같다.

발생한 지점에서 바로 에러를 처리하면 최하위의 모든 지점에서 에러를 처리해줘야하니 최상위로 보내서 처리하는 것이 좋겠다고 생각하였다. 이 프로젝트에서는 App 컴포넌트가 최상위가 되겠다.

try-catch를 이용한 에러 핸들링

  // theCatAPI.js
  /* eslint-disable no-useless-catch */
  const API_ENDPOINT = 'https://api.thecatapi.com/v1';

  const request = async url => {
      try {
          const response = await fetch(url);
          if(response.ok) {
              const data = await response.json();
              return data;
          } else {
              const errorData = await response.json();
              throw errorData;
          }
      } catch(e) {
          throw {
              message: e.message,
              status: e.status
          };
      }
  };

  const api = {
      fetchCats: async keyword => {
          /*
              keyword로 breed를 찾고 각 breed의 id로 이미지를 찾는다.
          */
          try {
              const breeds = await request(`${API_ENDPOINT}/breeds/search?q=${keyword}`);
              const requests = breeds.map(async breed => {
                  return await request(`${API_ENDPOINT}/images/search?limit=20&breed_ids=${breed.id}`);
              });
              const responses = await Promise.all(requests);
              const result = Array.prototype.concat.apply([], responses);

              return {
                  isError: false,
                  data: result
              };
          } catch(e) {
              return {
                  isError: true,
                  data: e
              };
          }
      },
      fetchRandomCats: async () => {
          /*
              랜덤으로 20개의 고양이 사진을 리턴한다.
          */
          try {
              const result = await request(`${API_ENDPOINT}/images/search?limit=20`);
              return {
                  isError: false,
                  data: result
              };
          } catch(e) {
              return {
                  isError: true,
                  data: e
              };
          }
      }
  };

  export { api };
  • 요구사항 중 하나가 api 요청을 async, await으로 처리하는 것이였다. 따라서 try-catch로 에러 핸들링을 하였다.
  • fetch APIrequest 함수 안에서만 사용된다.
    • responseok일 때만 정상 데이터가 리턴된다.
    • fetch API 요청에 대한 에러는 { message: String, status: String } 형태로 throw 된다.
  • 하위에서 던져진 에러는 상위로 전달되어 { isError: boolean, data: Object } 형태로 다시 상위로 throw 된다.
  const searchingSection = new SearchingSection({
              $target,
              keywords,
              onSearch: async keyword => {
                  loading.toggleSpinner();

                  const response = await api.fetchCats(keyword);
                  if(!response.isError){
                      setItem('data', response.data);
                      resultsSection.setState(response.data);
                      loading.toggleSpinner();
                  } else {
                      error.setState(response.data);
                  }
              },
              onRandom: async () => {
                  loading.toggleSpinner();

                  const response = await api.fetchRandomCats();
                  if(!response.isError){
                      setItem('data', response.data);
                      resultsSection.setState(response.data);
                      loading.toggleSpinner();
                  } else {
                      error.setState(response.data);
                  }
              }
  });
  • 최상위인 App 컴포넌트로 전달된 에러는 Error 컴포넌트로 전달되어 error code에 따라 다른 에러 화면으로 랜더링된다.

error code에 따른 error 화면 출력하기

  // Error.js
  export default class Error {
      constructor({ $target }) {
          this.$target = $target;
          this.errorData = null;

          this.render();
      }

      setState(nextData){
          this.errorData = nextData;
          this.render();
      }

      render() {
          if(!this.errorData) return;

          this.$target.innerHTML = '';

          const errorSection = document.createElement('section');
          errorSection.className = 'error-section';

          const errorImage = document.createElement('img');
          errorImage.className = 'error-image'; 
          errorImage.src = '/src/img/squarecat.jpg';

          const statusCode = document.createElement('p');
          statusCode.className = 'status-code';
          statusCode.innerText = this.errorData.status;

          const errorMessage = document.createElement('p');
          errorMessage.className = 'error-message';
          errorMessage.innerText = this.errorData.message;

          const returnBtn = document.createElement('p');
          returnBtn.className = 'return-btn';
          returnBtn.innerText = '돌아가기';

          returnBtn.addEventListener('click', () => {
              location.reload();
          });

          errorSection.appendChild(errorImage);
          errorSection.appendChild(statusCode);
          errorSection.appendChild(errorMessage);
          errorSection.appendChild(returnBtn);

          this.$target.appendChild(errorSection);
      }
  }
  • App 컴포넌트로 전달된 에러 데이터는 Error 컴포넌트의 state를 변경시키고 화면에 Error 정보를 렌더링한다.
profile
그냥 개발자

4개의 댓글

comment-user-thumbnail
2020년 12월 30일

프론트엔드 개발 프로젝트를 어떻게 시작할지 막막하던 차에 너무 좋은 글 보고 깨달음 많이 얻고 갑니다. 아직 코린이지만 위 글에서 fetch()를 async/await이랑 같이 쓴 부분에 대해 궁금해서 글 남겨 봅니다. 제가 알기로 fetch()는 Response 객체를 래핑한 Promise 객체를 반환하는 함수여서 따로 async/await이나 try catch 문없이 Promise의 후속 처리 메서드then이랑 catch를 사용하여 동기적인 후속 처리나 에러처리가 가능한 것으로 알고 있는데 위와 같이 한 이유가 있을까요?
참고: https://velog.io/@sdc337dc/javascript-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%86%B5%EC%8B%A0-fetch

1개의 답글