에러핸들링의 중앙 집중화 [Errorboundary 와 리덕스]

Doodream·2022년 1월 1일
0

ChawChaw - 프로젝트

목록 보기
6/8
post-thumbnail

우리가 코드를 짜다보면 에러핸들링이라는 것을 많이 한다. 에러 핸들링은 이 동작에서 무엇인가 잘못 동작했을때 예외처리를 해서 쌩에러 문구가 뜨지 않게끔 하는 것인데,

이러한 에러핸들링을 하려면 매번 그 쓰이는 부분 마다 에러핸들링을 해줘야 하는 번거로움이 있다.

그래서 리덕스를 도입하기전 훅으로 비동기 함수들의 예외처리를 하며 중복코드를 최대한 줄이려고 이러한 로직을 세웠다.

  const sendPost = async <T>(url: string, body: Object) => {
    try {
      const result = await request.post<DefaultResponseBody<T>>(url, body);
      console.log(result);
      return result.data;
    } catch (err) {
      catchError(err);
      throw err;
    }
  };

  const sendGet = async <T>(url: string, params?: Object) => {
    try {
      const result = await request.get<DefaultResponseBody<T>>(url, {
        params: params,
      });
      console.log(result);
      return result.data;
    } catch (err) {
      catchError(err);
      throw err;
    }
  };

  const sendDelete = async <T>(url: string, body?: Object) => {
    try {
      const result = await request.delete<DefaultResponseBody<T>>(url, {
        data: body,
      });
      console.log(result);
      return result.data;
    } catch (err) {
      catchError(err);
      throw err;
    }
  };

  const sendFormData = async <T>(url: string, formData: FormData) => {
    try {
      const result = await request.post<DefaultResponseBody<T>>(url, formData, {
        headers: { "Content-Type": "multipart/form-data" },
      });
      console.log(result);
      return result.data;
    } catch (err) {
      catchError(err);
      throw err;
    }
  };

  const errorHandling = <T>({ err }: ErrorHandlingProps<T>) => {
    console.log(err.response, "errorHandling");
    if (!err.response) throw err;
    const { status } = err.response.data;

    if (!ERROR_CODES[status]) {
      // message.error(EXCEPT_ERRORCODES_MSG);
      return;
    }

    if (status === "C401") {
      return;
    }

    if (status === "U402") {
      return;
    }
    if (status === "T401" || status === "G403") {
      // message.error(ERROR_CODES[status].message, {
      //   onClose: () => {
      //     window.localStorage.removeItem("accessToken");
      //     window.localStorage.removeItem("expireAtAccessToken");
      //     window.localStorage.removeItem("user");
      //     window.location.href = LOGIN_PAGE_URL;
      //   },
      // });
    } else {
      // message.error(`${ERROR_CODES[status].message}`);
    }
  };

  const catchError = (err: AxiosError<DefaultResponseBody<any>>) => {
    if (axios.isAxiosError(err)) {
      errorHandling({ err });
    } else {
      // message.error(EXCEPT_ERROR_MSG);
      console.error(err);
    }
  };

보면 중복되는 로직도 많고 굳이 에러핸들링을 두단계에 걸쳐서 하는 이상한 코드라서 디버깅 할때 에러가 어디에서 처음 걸러지는지 확인하고 하는 필요없는 작업을 해야한다.

이때 왜 이런 비효율적인 로직을 짰냐면, message 라고 보이는 부분이 훅이였기 때문이다.

import {useAlert} from "react-alert"
const message = useAlert();
message.error("에러 메세지");

react-alert 라는 라이브러리는 커스텀 confirm 창을 쉽게 만들수 있게 해준다. 하지만 기반이 contextAPI 엿기 때문에 훅으로 구현이 되버리기 때문에 사용이 저 한줄 써서 사용자에게 커스텀 confirm 창을 띄울 수도 있지만 훅규칙을 지켜야 하는 제한사항도 생긴다.

따라서 나는 비동기 함수를 구현할때 해당 함수들을 훅으로 구현할 수 밖에없엇고 에러핸들링도 훅안에 넣어야 했다.

엄청나게 많은 api들의 기본적인 에러들을 한번에 거르고 싶어서 굳이 저렇게 두단계로 나눈것이다.
그리고 response의 정상적인 응답에 따른 핸들링은 컴포넌트 단위에서 하려고 말이다.

하지만 이 에러핸들링은 리덕스와 Errorboundary를 도입하고 훨씬 간결해졌다.


postActions.ts

export const like = createAsyncThunk("post/like", async (userId: number) => {
  await request.post(LIKE_API_URL, { userId });
});

PostModal.tsx

  const handleClickLike: MouseEventHandler<HTMLButtonElement> = async (e) => {
    try {
      e.preventDefault();
      await dispatch(like(props.id));
      setIsActiveLike(true);
    } catch (error) {
      dispatch(asyncErrorHandle(error));
    }
  };

Errorboundary

class Errorboundary extends Component<Props, States> {
  state: States = {
    notificationElement: null,
  };
  componentDidCatch(error: Error) {
    this.props.updateAlert({
      name: error.name,
      message: error.message,
    });
  }

  componentDidMount() {
    this.setState({
      notificationElement: document.getElementById("notification"),
    });
  }
  render() {
    const alert = this.props.alert;
    return (
      <>
        {alert.length !== 0 &&
          this.state.notificationElement &&
          ReactDOM.createPortal(
            <AlertMessage />,
            this.state.notificationElement
          )}
        {this.props.children}
      </>
    );
  }
}

에러바운더리와 리덕스를 이용해서 로직이 간결해졌다.
개선점을 말해본다.

  1. 에러바운더리의 특징으로 일반적인 컴포넌트 에러또한 사용자에게 커스텀 확인창으로 띄울수 있다.
  2. 리덕스의 특징으로 현재 일어나는 에러들을 store 에 저장해서 에러들을 로깅할 수 있다.
  3. 컴포넌트 단위에서의 try catch 로 인해 axios 부터 각 api들의 핸들링 까지 한번에 해버릴 수 있다.
  4. 로직이 간결 해지면서 코드 가독성이 훨씬 높아져 유지보수성이 높아졌다.
  5. 더이상 훅규칙내에서 에러 메세지를 보낼필요가 없다. dispatch만 동작한다면 어떤 로직이든 에러 메세지를 띄울 수 있다.

즉, 모든 에러를 store에 모아서 중앙 집중화를 통해 에러를 관리 할수 있게 되어 다른 코드를 넣을 것 없이 try catch 에 dispatch 한번이면 비동기 함수부터 컴포넌트 에러까지 모든 에러를 핸들링 하게 된다.

심지어 확인 버튼을 누르면 동작하는 함수까지 구현가능해서 이전에 훅으로 구현할 때랑 기능적인 차이는 없으나 코드관리는 훨씨 간편해졌다.

코드를 짜기 훨씬 편해진접은 바로 5번이였다. 더이상 컴포넌트 최상단, 훅안에서만 사용해야한다같은 규칙을 버릴 수 있어, 원하는 코드를 이상적으로 로직화 할 수 있었다.

물론 이게 좋은 로직이라고 자신을 할 수는 없다. 하지만 이전보다 더 나은 전략이라고는 확신한다. 추후 클린코드를 공부하면서 에러 핸들링과 로직에 대해서 깊게 고민해볼 예정이다.


직면한 문제들

  • 리덕스 store 는 함수를 저장 할수 없어서 (non-serilization) 컴포넌트 단위에서 사용자에게 확인버튼을 눌렀을 때 동작시킬 함수를 전달 할수가 없었다. -> 이부분은 함수마다 action 상수를 줘서 해결했다.
  • Errorboundary는 컴포넌트에서 동작하는 에러만 잡을 수 있었다. -> 리덕스 store에서 error 집중화 전략을 통해 비동기 에러를 throw 하는 것이 아닌 dispatch 하여 잡을 수 있었다.
profile
일상을 기록하는 삶을 사는 개발자 ✒️ #front_end 💻

0개의 댓글