react todo app 2차 리팩토링 2/2

권현경·2022년 11월 19일
1

1. 비동기처리

현재는 성공한 코드에 대한 처리만 되어있고 로딩이나 에러에 관한 처리가 되어있지 않은 상태이다. 작업을 하기 전에 꼭 보면 좋을 영상을 추천받아서 먼저 보게 되었다. 이어질 내용은 토스 | slash21 - 프론트엔드 웹 서비스에서 우아하게 비동기 처리하기 영상에 나온 내용이다.

function fetchAccounts(callback) {
	fetchUserEntity((err, user) => {
    	if (err != null) {
        	callback(err, null);
          	return;
        }
      	
      	fetchUserAccounts(user.no, (err,accounts) => {
          if (err != null) {
          	callback(err, null);
            return;
          }
          callback(null, accounts);
        });
    });
}

위 코드의 문제점은 성공하는 경우와 실패하는 경우가 분리되어 있지 않다는 것이다. 두 경우가 섞여서 처리되면 함수의 진짜 역할이 가려지고 코드를 작성하는 입장에서 매번 에러 유무를 확인해야 한다.

async function fetchAccounts() {
	const user = await fetchUserEntity();
  	const accounts = await fetchUserAccounts(user.no);
  	return accounts;
}

반면 위의 코드는 성공하는 경우만 다루고 실패하는 경우는 catch 절에서 분리해서 처리할 수 있다. 실패할 수 있는 코드에 대한 처리를 외부에 위임할 수 있는 것이다.

이렇게 살펴본 좋은 코드의 특징은 아래와 같다.

  1. 성공, 실패의 경우를 분리해 처리할 수 있다.
  2. 비즈니스 로직을 한눈에 파악할 수 있다.

React 컴포넌트에서의 비동기 처리

우리는 기존에 swr이나 react-query등을 사용해서 비동기 처리를 구현해왔다. (굉장히 찔리는 대목이었다..)

function Profile() {
	const foo = useAsyncValue(()=>{
    	return fetchFoo();
    });
  
  	if(foo.error) return <div>로딩에 실패했습니다.</div>
    if(!foo.data) return <div>로딩중입니다...</div>
    return <div>{foo.data.name}님 안녕하세요</div>
}

성공하는 경우와 실패하는 경우가 섞여서 처리되고 있었음을 알수있다. 이렇게 되면 실패하는 경우에 대한 처리를 외부에 위임하기가 어렵다고 한다. 이러한 문제는 여러개의 비동기 작업이 동시에 실행되면 더 심화된다.

React의 비동기 처리는 어렵다.

  • 성공하는 경우에만 집중해 컴포넌트를 구성하기 어렵다.
  • 2개 이상의 비동기 로직이 개입할 때, 비즈니스 로직을 파악하기 점점 어려워진다.

React Suspense로 문제 해결하기

<ErrorBoundary fallback={<MyErrorPage />}>
	<Suspense fallback={<Loader />}>
    	<Profile />
    </Suspense>
</ErrorBoundary>

위와 같이 Profile 컴포넌트를 ErrorBoundarySuspense로 감싸게 되면 에러 상태와 로딩 상태를 분리할 수 있다. 에러는 MyErrorPage 컴포넌트에서, 로딩 처리는 Loader 컴포넌트에서 처리하면 된다. 이렇게 하면 마치 동기적인 코드처럼 깔끔하게 처리할 수 있다.

현재 프로젝트에서 사용하고 있는 react-query에서 error 처리를 옵션으로 제공해주니 사용해봐야겠다.

2. 에러 구분하기

에러를 구분해보니 페이지로 이동시켜줘야 할 경우와 snackbar 메시지만 띄워줘야 할 경우가 있었다.
작업을 하다보니 react-router-dom 설정 - queryClient 설정 - 각 요청마다 재설정 이렇게 돌아돌아 설정을 하게 되었는데 그 과정을 정리해보았다.

에러 돌아돌아 설정하기

1. react-router-dom 설정

내가 사용한 react-router-dom의 createBrowser에서는 라우팅시 에러가 생기면 자동으로 그들이 만들어 놓은 페이지로 이동시켰다. 못생긴 페이지로 말이다. 이것을 커스텀하려면 errorElement라는 걸 추가해주어야 했다.

const router = createBrowserRouter([
  {
    path: MAIN_URL,
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      { index: true, loader: mainLoader },
      {
        path: LOGOUT_URL,
        loader: logoutLoader,
      },
      ...
  },
]);

추가한 errorElement에서 메시지를 상황별로 보여줘야 할 것 같은데 어떻게 할지 막막했다. react query 공식문서의 예시에서 힌트를 얻을 수 있었다.

아래와 같이 useRouterError 훅을 사용해서 라우팅시 생기는 에러를 받아올 수 있었고 따로 interface를 정의해주었다. 꼭 라우팅 에러가 아닌 axiosError가 발생한다해도 모두 이 페이지로 받아오므로 AxiosError도 따로 정의해주었다.

import * as React from "react";
import { useRouteError } from "react-router-dom";
import { AxiosError } from "axios";

interface RouteError {
  statusText: string;
  message: string;
}

interface CustomAxiosError extends AxiosError<{ details: string }> {}

export default function Error() {
  const error = useRouteError();
  const isAxiosError = error instanceof AxiosError;

  return (
    <div>
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>
        <i>
          {isAxiosError
            ? (error as CustomAxiosError)?.response?.data?.details
            : (error as RouteError).statusText || (error as RouteError).message}
        </i>
      </p>
    </div>
  );
}

2. queryClient 설정

위에서 말했듯이 어떤 에러가 발생하더라도 모두 에러페이지로 이동하므로 이동할 필요가 없을때를 따로 설정해줄 필요가 있었다. 예를 들어 로그인시 비밀번호가 틀려서 생기는 에러와 같은 경우에 말이다.

QueryClient의 useErrorBoundary를 true로 설정해줘서 네트워크 에러를 reactQuery에서 처리해줄 수 있도록 하였다.

const client = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0,
      useErrorBoundary: true,
    },
  },
});

3. 각 요청마다 재설정

하지만 이때 500에러는 따로 에러페이지로 이동시켜줘야하므로 각 요청 query와 mutation에 아래와 같이 useErrorBoundary를 재설정해주었다.

export const useDeleteTodoItem = (id: string) => {
  return useMutation({
 	...
    useErrorBoundary: (error) =>
      !!error.response ? error.response?.status >= 500 : false,
  });
};

화면별 케이스


로그인

  1. 400) 비밀번호나 이메일을 올바르게 입력하지 않았을 때 -> 로그인에 실패했습니다.
  2. 500대 에러) 서버 에러

회원가입

  1. 409) 이미 존재하는 유저입니다.
  2. 500대 에러) 서버 에러

리스트

  1. 400 ) 토큰 없이 요청을 보냈을 때 token is missing
  2. 500대 에러) 서버 에러
  3. 토큰 관련 에러(적용X)
  • 원래대로라면 토큰을 내 마음대로 수정해서 보냈을 때 에러가 나야하지만 나지 않는걸로보아 따로 구현이 되어있지 않은 것 같았다. 이건 나중에 서버 쪽 코드를 만져서 한번 수정해봐도 좋을 것 같다.

리스트 상세

  1. 400
  • 토큰 없이 요청을 보냈을 때 -> token is missing
  • 유효하지 않은 id 값으로 상세 조회를 했을 때 -> todo를 찾는 도중 문제가 생겼습니다
  1. 500대 에러) 서버 에러
  2. 토큰 관련 에러(적용X)

존재하지 않는 페이지

  1. 404) 유저가 존재하지 않는 페이지에 접속 -> 존재하지 않는 페이지입니다.
profile
프론트엔드 개발자

0개의 댓글