CHZZK-Study / Grass-Diary-Client

취지직 2팀 프로젝트 Grass Diary
1 stars 3 forks source link

✨feat: 에러 처리 수정 #215

Closed rkdcodus closed 2 weeks ago

rkdcodus commented 2 weeks ago

✅ 체크리스트

📝 작업 상세 내용

1️⃣ 전역 에러 처리

에러가 발생했을 때 전부 에러페이지로 이동하던 방식을 변경했습니다. get 요청 실패 시, 데이터 자체를 불러올 수 없기 때문에 페이지로 보여주었고 post/delete/patch 요청 실패 시에는 에러메시지를 토스트로 보여주도록 했습니다.

// main.tsx

const App = () => {
  const { handleError } = useApiError();

  const [queryClient] = useState(
    new QueryClient({
      defaultOptions: {
        mutations: {
          onError: handleError,
          networkMode: 'always',
        },
        queries: {
          networkMode: 'always',
        },
      },
    }),
  );

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools />
      <RecoilRoot>
        <GlobalStyle />
        <RouterProvider router={router} />
      </RecoilRoot>
    </QueryClientProvider>
  );
};

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

Main.tsx입니다. 에러 발생 시 Mutations에 한해서 handleError를 실행시켜 토스트 에러메시지가 뜨도록 했습니다. get 요청 실패(usequery)했을 때는 전역으로 처리하지 않았습니다. 페이지로 이동에는 useNavigate 훅이 필요한데 이건 Router 내부에서만 사용할 수 있기 때문에 기존 방식 그대로 api 훅 내부에 작성해주었습니다.

Mutaions 에러 발생시

const useApiError = () => {
  const { toast } = useToast();
  const handleError = useCallback((error: unknown) => {
    if (axios.isAxiosError(error)) {
      if (error.response) {
        const errorMessage = error.response.data.description;
        console.error(error);
        toast(errorMessage);
      }
    }
  }, []);
  return { handleError };
};

2️⃣ router에 errorElement 설정

const router = createBrowserRouter([
  {
    element: <Layout />,
    children: [
      { path: '/', element: <Intro /> },
      { path: '/main', element: <Main /> },
      { path: '/share', element: <Share /> },
    ],
    errorElement: <NotFoundPage />,
  },

  {
    element: <ProtectedRoute />,
    children: [
      {
        path: '/creatediary',
        element: <CreateDiary />,
        errorElement: <NotFoundPage />,
      },
      { path: '/editdiary/:diaryId', element: <EditDiary /> },
      { path: '/diary/:diaryId', element: <DiaryDetail /> },
      {
        path: '/setting',
        element: <Setting />,
        errorElement: <NotFoundPage />,
      },
      { path: '/mypage', element: <MyPage />, errorElement: <NotFoundPage /> },
      {
        path: '/errorpage',
        element: <ErrorPage />,
        errorElement: <NotFoundPage />,
      },
      {
        path: '/rewardpage',
        element: <RewardPage />,
        errorElement: <NotFoundPage />,
      },
    ],
  },
]);

잘못된 path 접근 시 <NotFoundPage /> 페이지를 보여주기 위해 errorElement를 설정했습니다.

image

추가 ) 모바일 Ui 추가 및 일부 색상 변경되었습니다!

3️⃣ 일기 중복 작성 토스트 알림 타이밍 변경

작성페이지에 접근하기 전에 이미 작성되어있는 일기 있는지 판단하여 사용자에게 메시지를 보여주는 것으로 변경되었습니다.

const TopSection = () => {
  // 질문 데이터를 가져오는 쿼리
  const { question } = useTodayQuestion();
  // 날짜 데이터를 가져오는 쿼리
  const { date } = useTodayDate();
  const { toast } = useToast();
  const navigate = useNavigate();

  const checkWritingPermission = () => {
    if (date) {
      const lastWritingDate = localStorage.getItem('lastWritingDate');
      const currentDate = `${date.year}년/${date.month}월/${date.date}일`;

      return lastWritingDate === currentDate
        ? toast(CREATE_MESSAGES.toast.already_written)
        : navigate('/creatediary');
    }
  };
  ...

메인페이지의 일기 쓰기 버튼을 눌렀을 때 checkWritingPermission 함수가 실행됩니다.

4️⃣ 에러 모달 변경 사항 및 네트워크 에러

로그인 만료가 되면 memberId를 받아올 수 없어 401에러가 뜨면서 지정된 에러메시지를 보여주었었습니다. image 대부분 엑세스 토큰 만료로 인한 401에러가 뜰 것으로 예상되지만 다른 메시지도 보여줄 수 있도록 서버에서 전달받은 에러메시지를 보여주도록 변경했습니다. Header에서는 에러메시지가 제대로 뜨지 않아서 모달 관련 코드를 useUser 내부로 옮겼습니다.

또한 Api.interceptor가 401에러를 발견하고 소개페이지로 바로 이동할 경우 모달이 뜨지 않아서 모달의 확인 버튼을 클릭 했을 때 소개페이지가 이동하도록 했습니다.

// useUser.ts 

if (isError) {
    const manualLogout = localStorage.getItem('manualLogout');

    if (manualLogout === null) {
      const setting = {
        title: isNetworkOffline
          ? MODAL.network_error.title
          : MODAL.authentication_error.title,
        content: isNetworkOffline
          ? MODAL.network_error.content
          : `${error.response?.data.description}\n` + '다시 로그인 해주세요',
      };

      const button1 = {
        active: true,
        text: MODAL.confirm,
        color: semantic.light.accent.solid.hero,
        interaction: INTERACTION.accent.subtle(),
        clickHandler: () => (window.location.href = '/'),
      };

      modal(setting, button1);
    }
  }

useNetwork 훅

export const useNetwork = () => {
  const [isNetworkOffline, setIsNetworkOffline] = useState(!navigator.onLine);

  const handleOnOffLine = () => {
    setIsNetworkOffline(() => !navigator.onLine);
  };

  useEffect(() => {
    window.addEventListener('online', handleOnOffLine);
    window.addEventListener('offline', handleOnOffLine);

    return () => {
      window.removeEventListener('online', handleOnOffLine);
      window.removeEventListener('offline', handleOnOffLine);
    };
  }, []);

  return isNetworkOffline;
};

2024-10-06 17;40;59

🚨 버그 발생 이유 (선택 사항)

image 로그아웃하고 곧바로 헤더의 로그인 버튼을 누르면 모달이 비정상적으로 뜨는 버그를 발견했습니다. Header 컴포넌트가 렌더링하는 동안 Modal 컴포넌트를 업데이트할 수 없는데 Header 컴포넌트가 렌더링을 완료하지 않은 상태에서 Modal 컴포넌트가 실행되었기 때문에 업데이트되지 않은 모달이 떴던 것이였습니다. 실제로 로그아웃하고 한 3초정도 기다린 후 로그인 버튼을 누르면 정상적인 모달이 뜹니다.

해결

로그아웃하면 memberId를 가져오는 api 요청이 실패하면서 401 에러가 뜨면서 refetch를 1번 더 실행합니다. 재시도를 하지 않도록 설정하고 Header 렌더링이 빨리 끝나도록 해서 모달이 빨리 업데이트 되도록 했습니다. 더 좋은 방법은 없을지.. 고민입니다!

🔎 후속 작업 (선택 사항)

🤔 질문 사항 (선택 사항)

📚 참고 자료 (선택 사항)

React Error Boundary를 사용하여 에러 핸들링하기(react-query) TanStack Query v5를 사용해 API 호출 시 발생한 에러 핸들링하기 (+ 네트워크 에러)

📸 스크린샷 (선택 사항)

변경 사항에 대한 스크린샷이 있다면 첨부해주세요.

✅ 셀프 체크리스트

이슈 번호: Close #107