Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/error boundary #1

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
"react-router-dom": "^6.22.1",
"zustand": "^4.5.1"
},
Expand Down
18 changes: 18 additions & 0 deletions src/components/Quiz/Quiz.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { QuizCard } from "@components/QuizCards";
import { useQuizStore } from "@store/quizStore";

export const Quiz = () => {
const { userAnswerList, setUserAnswer, quizList } = useQuizStore();
const currentQuiz = quizList[userAnswerList.length];

return (
<QuizCard
quiz={currentQuiz}
total={quizList.length}
current={userAnswerList.length}
handleNextButton={(answer) => {
setUserAnswer(answer);
}}
/>
);
};
43 changes: 43 additions & 0 deletions src/components/QuizFallbackWithSample/QuizFallbackWithSample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { SAMPLE_SHUFFLED_QUIZ_LIST } from "@/data/quizSampleData";
import { Button } from "@components/Buttons";
import { Heading } from "@components/Heading";
import { useQuizStore } from "@store/quizStore";
import { ReactElement, useState } from "react";
import { FallbackProps } from "react-error-boundary";

type Props = {
Component: ReactElement;
} & FallbackProps;

export const QuizFallbackWithSample = ({
resetErrorBoundary,
Component,
}: Props) => {
const { setQuizList } = useQuizStore();

const [viewSample, setViewSample] = useState(false);
const handleSampleDataButtonClick = () => {
setQuizList(SAMPLE_SHUFFLED_QUIZ_LIST);
setViewSample(true);
};

if (viewSample) {
return Component;
}

return (
<div className="mt-14 flex flex-col items-center gap-4 text-center">
<div className="mb-4 text-8xl">❗️</div>
<Heading level={2}>네트워크 에러가 발생했습니다.</Heading>

<div className="flex w-full max-w-96 flex-col gap-3">
<Button variant="outlined" onClick={resetErrorBoundary}>
재시도
</Button>
<Button onClick={handleSampleDataButtonClick}>
샘플 데이터로 계속 진행하기
</Button>
</div>
</div>
);
};
56 changes: 33 additions & 23 deletions src/data/quizSampleData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,49 @@ export const EMPTY_SHUFFLED_QUIZ: ShuffledQuiz = {
};

export const SAMPLE_SHUFFLED_QUIZ_LIST: ShuffledQuiz[] = [
{
type: "multiple",
difficulty: "easy",
category: "문화",
question: "세계에서 가장 많이 사용되는 언어는 무엇인가?",
correct_answer: "영어",
incorrect_answers: ["중국어", "스페인어", "아랍어"],
shuffledAnswers: ["중국어", "스페인어", "아랍어", "영어"],
},
{
type: "multiple",
difficulty: "medium",
category: "Entertainment: Film",
question:
"Which actor and martial artist starred as Colonel Guile in the 1994 action film adaptation of Street Fighter?",
correct_answer: "Jean-Claude Van Damme",
incorrect_answers: ["Chuck Norris", "Steven Seagal", "Scott Adkins"],
shuffledAnswers: [
"Jean-Claude Van Damme",
"Chuck Norris",
"Steven Seagal",
"Scott Adkins",
],
category: "천문학",
question: "태양계에서 가장 큰 행성은?",
correct_answer: "목성",
incorrect_answers: ["토성", "천왕성", "지구"],
shuffledAnswers: ["천왕성", "목성", "토성", "지구"],
},
{
type: "multiple",
difficulty: "easy",
category: "엔터테인먼트",
question: "'심슨 가족'에서 심슨 가족의 개 이름은?",
correct_answer: "산타의 작은 도우미",
incorrect_answers: ["맥스", "루디", "버디"],
shuffledAnswers: ["맥스", "버디", "루디", "산타의 작은 도우미"],
},
{
type: "multiple",
difficulty: "medium",
category: "Entertainment: Japanese Anime &amp; Manga",
question:
"In &quot;Puella Magi Madoka Magica&quot;, what is the first name of Madoka&#039;s younger brother?",
correct_answer: "Tatsuya",
incorrect_answers: ["Montoya", "Tomohisa", "Minato"],
shuffledAnswers: ["Montoya", "Tomohisa", "Minato", "Tatsuya"],
category: "기술",
question: "애플의 공동 창업자는 스티브 잡스와 누구인가?",
correct_answer: "스티브 워즈니악",
incorrect_answers: ["론 웨인", "마이클 델", "빌 게이츠"],
shuffledAnswers: ["론 웨인", "빌 게이츠", "마이클 델", "스티브 워즈니악"],
},
{
type: "multiple",
difficulty: "easy",
category: "Entertainment: Film",
question:
"In the movie &quot;Spaceballs&quot;, what are the Spaceballs attempting to steal from Planet Druidia?",
correct_answer: "Air",
incorrect_answers: ["The Schwartz", "Princess Lonestar", "Meatballs"],
shuffledAnswers: ["The Schwartz", "Princess Lonestar", "Air", "Meatballs"],
category: "패션",
question: "티셔츠에서 'T'는 무엇을 의미하는가?",
correct_answer: "모양",
incorrect_answers: ["질감", "트렌드", "타입"],
shuffledAnswers: ["모양", "트렌드", "질감", "타입"],
},
];
19 changes: 19 additions & 0 deletions src/hocs/withAsyncBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactElement, Suspense, type FC } from "react";
import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary";

type WithAsyncBoundary = <P extends object>(
Component: FC<P>,
fallbacks: {
pendingFallback: ReactElement;
rejectedFallback: ErrorBoundaryProps;
},
) => FC<P>;

export const withAsyncBoundary: WithAsyncBoundary =
(Component, fallbacks) => (props) => (
<Suspense fallback={fallbacks.pendingFallback}>
<ErrorBoundary {...fallbacks.rejectedFallback}>
<Component {...props} />
</ErrorBoundary>
</Suspense>
);
40 changes: 21 additions & 19 deletions src/pages/QuizPage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { withSuspense } from "@/hocs/withSuspense";
import { QuizCard } from "@components/QuizCards";
import { withAsyncBoundary } from "@/hocs/withAsyncBoundary";
import { Quiz } from "@components/Quiz/Quiz";
import { SkeletonQuizCard } from "@components/QuizCards/QuizCard/QuizCard.skeleton";
import { QuizFallbackWithSample } from "@components/QuizFallbackWithSample/QuizFallbackWithSample";
import { useQuizListQuery } from "@service/useQuizService";
import { useQuizStore } from "@store/quizStore";

export const QuizPage = withSuspense(() => {
useQuizListQuery();
const { userAnswerList, setUserAnswer, quizList } = useQuizStore();
const currentQuiz = quizList[userAnswerList.length];

return (
<QuizCard
quiz={currentQuiz}
total={quizList.length}
current={userAnswerList.length}
handleNextButton={(answer) => {
setUserAnswer(answer);
}}
/>
);
}, SkeletonQuizCard);
export const QuizPage = withAsyncBoundary(
() => {
useQuizListQuery();
return <Quiz />;
},
{
pendingFallback: <SkeletonQuizCard />,
rejectedFallback: {
fallbackRender: ({ error, resetErrorBoundary }) => (
<QuizFallbackWithSample
error={error}
resetErrorBoundary={resetErrorBoundary}
Component={<Quiz />}
/>
),
},
},
);
3 changes: 2 additions & 1 deletion src/service/quizService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { QuizPayload, QuizResponse } from "@models/quiz";
import axios from "axios";
import axiosRetry from "axios-retry";

const API_URL = "https://opentdb.com/api.php";
// TODO: 네트워크 에러 발생 유도
const API_URL = "https://opentdb.com/api.php" + "error";
Comment on lines +5 to +6
Copy link
Owner Author

Choose a reason for hiding this comment

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

Suggested change
// TODO: 네트워크 에러 발생 유도
const API_URL = "https://opentdb.com/api.php" + "error";
const API_URL = "https://opentdb.com/api.php";

❗️ 테스트 발생 유도를 위한 작업입니다.

const TIMEOUT_MILLISECOND = 6 * 1000; // 6초

const axiosInstance = axios.create();
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8104,6 +8104,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"

react-error-boundary@^4.0.12:
version "4.0.12"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.12.tgz#59f8f1dbc53bbbb34fc384c8db7cf4082cb63e2c"
integrity sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==
dependencies:
"@babel/runtime" "^7.12.5"

react-error-overlay@^6.0.11:
version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
Expand Down