THE DEVLOG

scribbly.

Next.js+형태소분석 CS퀴즈앱 만들기

2023.03.05 23:07:23

얼추 퀴즈의 규칙도, 퀴즈를 푸는 화면의 레이아웃도 디자인이 끝났다.

본 어플리케이션을 PWA로, 오프라인에서도 작동하게 만들 예정이다.

따라서 LocalStorage를 적극적으로 활용할 생각이며,

MVP로 오늘 중으로 완성하여 빠르게 배포할 생각이다.

이를 바탕으로 사용자 경험을 아래와 같이 설정하였다.

  1. 접속하자마자 랜딩페이지가 뜬다. 랜딩페이지에서는 LocalStorage에 문제 데이터가 있는지를 확인한다.
  2. 랜딩페이지 다음장에서는 5개의 문제를 준다. 5개의 문제를 푼 상태를 상단에 보여주며, 5번의 시도만에 5개의 보기를 모두 맞추면 초록불, 10번의 시도 안에 5개의 보기를 모두 맞추면 노란불, 10번의 시도를 넘기면 빨간불이 뜬다.
  3. 문제 풀이가 끝나면 '내가 푼 문제'를 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}