본문 바로가기

프론트엔드

js try/catch문으로 함수 리팩토링하기

  • 키워드: try...catch, throw, Error object, refactoring
  • 상황
    1. GET 요청할 데이터를 전처리하고, 요청하고, 후처리하는 중요한 함수의 흐름을 파악하기 힘들어 함수 리팩토링을 진행해야한다.
        const searchData = () => {
        const ids = Array.from(new Set(idsString.split(',').map(Number)));
        if (ids.some(Number.isNaN) || ids.length < MINIMUM_COUNT) {
          toastErrorMessage(`'${MINIMUM_COUNT}개' 이상의 '숫자'를 입력해주세요.`);
        } else {
          setIsLoading(true);
          Api.getData({ ids: ids.join(',') })
            .then((response) => {
              const data = response.data.items;
              if (data.length < MINIMUM_COUNT) {
                throw Error(`${MINIMUM_COUNT}개 이상의 데이터를 찾지 못했습니다.`);
              } else {
                onEmitResult(ids, data);
              }
            })
            .catch((e) => {
              toastErrorMessage(e.response?.data ?? e);
            })
            .finally(() => {
              setIsLoading(false);
            });
          }
        };
      (일부 변수명과 함수명은 실제와 다르게 변경함)
    2. 함수명은 searchData이지만, 1) validate 로직을 실행하고 있고, 2) api call을 하는 메인 로직이 있고, 3) 응답값을 emit하는 함수를 호출하고 있다.
    3. 위의 함수를 함수 분리와 에러 핸들링을 통해 리팩토링하고자 한다.
  • 해결 방법
    1. 하나의 함수가 상황 2에서 말한 3가지 일을 하고 있으므로, 함수를 쪼갤 수 있다.
        const searchAndEmitData = async () => {
        const ids = Array.from(new Set(idsString.split(',').map(Number)));
        if (!validateIds(ids)) return;
      
        try {
          setIsLoading(true);
          const data = await searchData(ids);
          onEmitResult(ids, data);
        } catch (e) {
          toastErrorMessage(e instanceof Error ? e.message : e.response?.data);
        } finally {
          setIsLoading(false);
        }
      };
      
      const validateIds = (ids) => {
        if (ids.some(Number.isNaN) || ids.length < MINIMUM_COUNT) {
          toastErrorMessage(`'${MINIMUM_COUNT}개' 이상의 '숫자'를 입력해주세요.`);
          return false;
        }
        return true;
      };
      
      const searchData = async (ids) => {
        const data = (await Api.getData({ ids })).data.items;
        if (data.length < MINIMUM_COUNT) {
          throw new Error(`${MINIMUM_COUNT}개 이상의 데이터를 찾지 못했습니다.`);
        }
        return data;
      };​
       메인 함수의 이름을 'searchAndEmitData'로 좀 더 정확하게 작성하고, validateIds와 searchData 함수를 분리했다.
      여기서 searchData 함수는 성공시 data를 return하고 실패시 Error 객체를 throw한다. 하지만 validateIds는 boolean 값을 return하며, 리턴값은 여전히 메인 함수에서 분기 처리해야하는 구조이다.(또한 validateIds 함수는 toastErrorMessage를 부르는 side effect까지 가지고 있어 로직에 통일성이 없다.)

    2. 따라서 validateIds에 대한 리팩토링을 추가로 진행했다.
        const searchAndEmitData = async () => {
        const idsWithDuplicate = idsString.split(',').map(Number);
        const ids = Array.from(new Set(idsWithDuplicate));
        setIsLoading(true);
      
        try {
          validateIds(ids);
          const data = await searchData(ids);
          onEmitResult(ids, data);
        } catch (e) {
          handleErrorWithToast(e);
        } finally {
          setIsLoading(false);
        }
      };
      
      const validateIds = (ids) => {
        if (ids.some(Number.isNaN) || ids.length < MINIMUM_COUNT) {
          throw new Error(`'${MINIMUM_COUNT}개' 이상의 '숫자'를 입력해주세요.`);
        }
      };
      
      const searchData = async (ids) => {
        const data = (await Api.getData({ ids })).data.items;
        if (data.length < MINIMUM_COUNT) {
          throw new Error(`${MINIMUM_COUNT}개 이상의 데이터를 찾지 못했습니다.`);
        }
        return data;
      };
       validateIds, searchData, onEmitResult 함수가 try문 안에 각각 한 줄씩 정리되었고, 에러 발생시 모두 catch문에 잡혀 handleErrorWithToast 함수를 실행하도록 리팩토링했다.(handleErrorWithToast 함수는 Error 객체의 타입(isAxios or not) 과 내용에 따라 적절한 메시지를 출력한다.)

    3. 메인로직을 분리하고 try문 안으로 옮겨 흐름을 알기 쉬워졌고, 동시에 예외 처리는 catch문 하나로 묶어 통일된 방식으로 처리할 수 있게 되었다!