Skip to content

[UNI-145] feat : 에러 바운더리 생성 및 POST 에러 핸들링 로직#88

Merged
jpark0506 merged 3 commits intofefrom
feat/UNI-145
Feb 9, 2025
Merged

[UNI-145] feat : 에러 바운더리 생성 및 POST 에러 핸들링 로직#88
jpark0506 merged 3 commits intofefrom
feat/UNI-145

Conversation

@dgfh0450
Copy link
Copy Markdown
Contributor

@dgfh0450 dgfh0450 commented Feb 9, 2025

#️⃣ 작업 내용

  1. �에러 바운더리 생성
  2. POST 요청 시, 에러 핸들링 로직

핵심 기능

커스텀 에러 바운더리 생성

  • Error 발생 시, 보여줘야 할 fallback을 생성하기 위한 Global ErrorBoundary를 구현해야 했습니다.
  • 기존 react에서도 권장하는 react-error-boundary 라이브러리가 존재하나 조금 더 자유로운 커스텀을 위해 직접 구현하였습니다.
  • getDerivedStateFromError를 사용하였습니다.
import React, { Component, ReactNode } from 'react';

interface ErrorBoundaryProps {
    fallback: ReactNode;
}

interface ErrorBoundaryState {
    hasError: boolean;
}

export default class ErrorBoundary extends Component<
    React.PropsWithChildren<ErrorBoundaryProps>,
    ErrorBoundaryState
> {
    constructor(props: React.PropsWithChildren<ErrorBoundaryProps>) {
        super(props);
        this.state = { hasError: false };
    }

    static getDerivedStateFromError(error: Error): ErrorBoundaryState {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return this.props.fallback;
        }

        return this.props.children;
    }
}
  • Suspense와 동일하게 fallback을 받아서 에러 발생 시, fallback을 보여줄 수 있도록 하였습니다.
  • Global ErrorBoundary 적용 결과입니다.
      <ErrorBoundary fallback={<ErrorPage />}>
            <Suspense key={location.key} fallback={fallback}>
	            
            </Suspense>
      </ErrorBoundary>

POST 요청 시, 에러 핸들링 로직

  • GET 요청의 경우, useSuspenseQuery를 사용하여 화면을 렌더링하기 위한 데이터를 Get 하지만, POST의 경우, POST의 result로 true, false boolean을 받아오기에, POST 요청에 대한 에러 처리ㅁㅁ 화면을 보여줄 필요가 있습니다.
  • 그리하여, POST를 위한 useMutation을 변형하여 새로운 Mutation훅을 생성하였습니다.
import { QueryClient, useMutation, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { NotFoundError, BadRequestError, ERROR_STATUS } from "../constant/error";
import React, { useCallback, useEffect, useState } from "react";

type Fallback = {
	[K in Exclude<ERROR_STATUS, ERROR_STATUS.INTERNAL_ERROR>]: {
		mainTitle: string;
		subTitle: string;
	};
};

export default function useMutationError<TData, TError, TVariables, TContext>(
	options: UseMutationOptions<TData, TError, TVariables, TContext>,
	queryClient?: QueryClient,
	fallback?: Fallback,
): [React.FC, UseMutationResult<TData, TError, TVariables, TContext>] {
	const [isOpen, setOpen] = useState<boolean>(false);
	const [title, setTitle] = useState({
		mainTitle: "",
		subTitle: "",
	});
	const result = useMutation<TData, TError, TVariables, TContext>(options, queryClient);

	const { isError, error } = result;

	useEffect(() => {
		if (isError) {
			setOpen(isError);
			if (error instanceof NotFoundError && fallback) {
				setTitle({ ...fallback[404] });
			} else if (error instanceof BadRequestError && fallback) {
				setTitle({ ...fallback[400] });
			} else {
				throw error;
			}
		}
	}, [error, isError]);

	const close = useCallback(() => {
		setOpen(false);
	}, []);

	const BaseFallback = (
		<div>
			<div>
				<div>
					<p>{title.mainTitle}</p>
					<div>
						<p>{title.subTitle}</p>
					</div>
				</div>
				<button onClick={close}>
					확인
				</button>
			</div>
		</div>
	);

	const Modal: React.FC = () => <>{isOpen && BaseFallback}</>;

	return [Modal, result];
}
  • useMutation이 기본적으로 입력받던, queryOptions, queryClient를 받고 추가적으로 fallback을 만들기 위한, 대제목과 소제목을 입력받습니다.

  • POST의 에러는 모달 형태로 보여줄 것이기에, mutation결과와 함께, Modal을 return 합니다.�

  • 현재는 400, 404에 대한 처리만 진행이 되었지만 추후 확장될 수 있습니다.

  • useMutationError를 사용하려면

        const [Modal400, result400] = useMutationError(
		{
			//@ts-expect-error 강제 에러 발생
			mutationFn: () => postReport(1001, 1, { cautionTypes: ["TEST"], dangerTypes: [] }),
		},
		undefined,
		{
			400: { mainTitle: "400 제목", subTitle: "400 부제목" },
			404: { mainTitle: "404 제목", subTitle: "404 부제목" },
		},
	);
  • 다음과 같이, fallback에 400, 404를 key로 하는 오브젝트를 전달합니다.

  • 400과 404의 에러는 에러 바운더리에서 잡히는 것이 아닌, useMutationError에서 잡혀야 하기 때문에 fetch에서 response를 통해 다른 타입의 에러를 throw합니다.

        const post = async <T, K>(url: string, body?: Record<string, K | K[]>): Promise<boolean> => {
		const response = await fetch(`${baseURL}${url}`, {
			method: "POST",
			body: JSON.stringify(body),
			headers: {
				"Content-Type": "application/json",
			},
		});

		if (!response.ok) {
			if (response.status === 400) {
				throw new BadRequestError("Bad Request");
			} else if (response.status === 404) {
				throw new NotFoundError("Not Found");
			} else {
				throw new Error("UnExpected Error");
			}
		}

		return response.ok;
	};
  • response가 ok가 아닐 경우, status를 비교합니다. status가 400인 경우, BadRequestError를, 404인 경우, NotFoundError를 throw 합니다. 그 외의 경우에는 일반적은 Error를 throw합니다.

  • useMutationError에서는 useEffect 내부에서 mutation결과 error를 instanceof로 비교합니다.

	useEffect(() => {
		if (isError) {
			setOpen(isError);
			if (error instanceof NotFoundError && fallback) {
				setTitle({ ...fallback[404] });
			} else if (error instanceof BadRequestError && fallback) {
				setTitle({ ...fallback[400] });
			} else {
				throw error;
			}
		}
	}, [error, isError]);
  • 에러가 BadRequestError 혹은 NotFoundError인 경우, error 핸들을 내부에서 하기 위해 state를 변경하지만 그 외의 에러는 throw하여 ErrorBoundary에서 감지될 수 있도록 하였습니다.

동작 확인

2025-02-09.3.16.06.mov

@dgfh0450 dgfh0450 added 🚀 feat 기능 개발 🧇 fe 프론트엔드 task labels Feb 9, 2025
@dgfh0450 dgfh0450 requested a review from jpark0506 February 9, 2025 08:14
@dgfh0450 dgfh0450 self-assigned this Feb 9, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 9, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@jpark0506 jpark0506 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM~ 좋은 코드를 작성해주셔서 많이 배울 수 있었습니다 고생하셨습니다!

Comment on lines +11 to +31
export default class ErrorBoundary extends Component<
React.PropsWithChildren<ErrorBoundaryProps>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<ErrorBoundaryProps>) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true };
}

render() {
if (this.state.hasError) {
return this.props.fallback;
}

return this.props.children;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static 메소드인 getDerivedStateFromError을 Override하는 방식이군요?? 커스텀 에러바운더리를 구현하시다니..대단합니다!! 하나 배워갑니다

import React, { useCallback, useEffect, useState } from "react";

type Fallback = {
[K in Exclude<ERROR_STATUS, ERROR_STATUS.INTERNAL_ERROR>]: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum 타입에서 특정 값을 제외하고 싶을 때 사용하는 용도로 사용하신 것 같은데, 다른 400에러가 추가될 수 있다는 점에서 확장성이 좋은 것 같습니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다!
저도 이부분은 생성형 AI의 도움을 받아서 해결할 수 있었습니다

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예시까지 주셔서 코드 작성하는데 많은 도움이 될 것 같습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧇 fe 프론트엔드 task 🚀 feat 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants