얼추 퀴즈의 규칙도, 퀴즈를 푸는 화면의 레이아웃도 디자인이 끝났다.
본 어플리케이션을 PWA로, 오프라인에서도 작동하게 만들 예정이다.
따라서 LocalStorage를 적극적으로 활용할 생각이며,
MVP로 오늘 중으로 완성하여 빠르게 배포할 생각이다.
이를 바탕으로 사용자 경험을 아래와 같이 설정하였다.
- 접속하자마자 랜딩페이지가 뜬다. 랜딩페이지에서는 LocalStorage에 문제 데이터가 있는지를 확인한다.
- 랜딩페이지 다음장에서는 5개의 문제를 준다. 5개의 문제를 푼 상태를 상단에 보여주며, 5번의 시도만에 5개의 보기를 모두 맞추면 초록불, 10번의 시도 안에 5개의 보기를 모두 맞추면 노란불, 10번의 시도를 넘기면 빨간불이 뜬다.
- 문제 풀이가 끝나면 '내가 푼 문제'를 LocalStorage에서 가져와 보여준다.
랜딩 페이지 로직
랜딩페이지는 busy waiting으로 로딩 여부를 확인하고 자동으로 이동하도록 하려고 했는데,
랜딩 페이지에서 처리할 요청이 하나 밖에 없고, 또 갑자기 문제풀이가 시작되는 것도 이상하여 '시작하기' 버튼을 누르도록 하였다.
pages\index.tsx
useEffect(() => {
fetchData().then((res) => {
const { quizes, keywords } = res;
localStorage.setItem("quizes", JSON.stringify(quizes));
localStorage.setItem("keywords", JSON.stringify(keywords));
setMyQuiz(JSON.parse(localStorage.getItem("myQuiz")));
setReady(true);
});
}, []);
return (
...
{ready ? (
<>
<Description>
<Button label="시작하기" onClick={(e) => router.push("/quiz")} />
</Description>
)
문제 풀이 로직
pages\quiz\index.tsx에 다섯개의 문제의 시도 횟수를 담는 배열을 만들었다. 적은 횟수 만에 문제를 맞출수록 좋은 점수를 받는다.
상단 바를 하나 만들고, 몇 번 만에 맞췄는지에 따라 색깔을 달리 주도록 하였다.
const [attemptsArray, setAttemptsArray] = useState([0, 0, 0, 0, 0]);
...
const nav = (
<StyledNav>
<div>문제번호: {quizNumber}</div>
<div className="attempts-flex">
{attemptsArray.map((attempts, idx) => {
let Squarebox;
const score = 5 / attempts;
if (score < 0.2) {
Squarebox = (
<div className="red-box" key={idx.toString() + attempts}>
■
</div>
);
} else if (score < 0.82) {
Squarebox = (
<div className="yellow-box" key={idx.toString() + attempts}>
■
</div>
);
} else if (score <= 1) {
Squarebox = (
<div className="green-box" key={idx.toString() + attempts}>
■
</div>
);
} else {
Squarebox = <div key={idx.toString() + attempts}>□</div>;
}
return Squarebox;
})}
</div>
</StyledNav>
);
이렇게 만들어진 nav바는 자식 컴포넌트에 넘겨서 자식 컴포넌트의 디자인 요소 안으로 넣기로 했다.
<QuizContainer
quiz={quiz}
keywords={keywords}
onCorrect={onCorrect}
nav={nav}
timer={timer}
/>
그 밖에 components\QuizContainer.tsx 안에서도 여러가지 디버깅이 이루어졌다. 문자열을 조작하다보니 Array.prototype.includes 메서드나 new Set(Array)가 제대로 작동하지 많아 Array.prototype.indexOf 메서드로 대체하였으며, 정답 보기가 5개 이하인 경우에 대해서도 예외처리를 했다.
한 문제를 풀 때마다 attempts 배열이 채워지므로, 5개가 모두 채워지면 문제를 다 풀었다고 알리고 다음 페이지로 넘어갈 예정이다.
그 전에 모달창을 하나 띄워서 5문제를 모두 풀 때까지 걸린 시간과 평균 시도 횟수를 보여주기로 한다.
useEffect(() => {
if (attemptsArray.indexOf(0) >= 0) return;
clearInterval(timerId.current);
setCleared(true);
}, [attemptsArray]);
{cleared ? (
<ClearPopup>
<div className="text-popup-wrapper">
<div>
평균 점수 :{" "}
{(
(25 / attemptsArray.reduce((acc, value) => acc + value)) *
100
).toFixed(0) + "%"}
</div>
<div>소요 시간 : {timer} 초</div>
<hr />
<Button
label="푼 문제 보기 >"
onClick={() => router.push("/quiz/myquiz")}
color="green"
/>
</div>
</ClearPopup>
) : (
<></>
)}
나의 문제 확인하기
나의 문제는 전체적으로 디자인을 재활용 했다.
필터는 미리 필터가 된 배열을 배열 안에 저장해두는 방식으로 구현했다. (반응 속도가 짱빠르다.)
useEffect(() => {
const myQuizes = JSON.parse(localStorage.getItem("myQuiz"));
if (!myQuizes) {
alert("푼 문제가 없습니다");
router.push("/");
} else {
setMyQuizes(myQuizes);
setQuizes(myQuizes);
setFilteredArray(
//필터를 미리 걸어서 배열을 만든다.
categoryArray.map((category) => {
return myQuizes.filter((myQuiz) => {
return myQuiz.category === category;
});
})
);
}
}, []);
const categoryArray = [
"리액트",
"프론트엔드 전반",
"HTML",
"CSS",
"자바스크립트",
"네트워크",
"운영체제",
];
const onClickCategory = (e) => {
//전체면 스냅샷을 quizes로 하고, 아니라면 categoryArray를 참조해서 filteredArray에서 데이터를 불러온다.
const index = categoryArray.indexOf(e.target.value);
if (e.target.value === "전체") return setQuizes(myQuizes);
else {
setQuizes(filteredArray[index]);
}
};
추가로 뒤로가기 버튼을 만들기 위해 새로운 디자인을 만들었다.
export const StyledFAB = styled.div`
position: absolute;
top: -50px;
padding: 5px 10px;
`;
이렇게 만든 버튼은.... 어디다 놓을지 애매해서 하단 바랑 연동시키기 위해 하단 바에 props로 넘겨주었다.
<QuizAnswerContainer FAB={FAB}>
components\common\molecules\QuizAnswerContainer.tsx
const QuizAnswerContainer = ({ FAB = <></>, ...props }) => {
const [activated, setActivated] = useState(true);
return (
<StyledQuizAnswers>
{FAB}