[리팩토링] 3주차 quiz & 정리

Soozynn·2021년 8월 16일
0

[ 3 주차 핵심 ]

  • 값이 들어있는 만큼 동적으로 요소 생성 & 지우기

  • 문제를 다음으로 어떻게 넘어가게 구현하는가?

  • JSON에 제시되어있던 값들을 어떻게 가져오는가? => data[dataIndex].choices[data[dataIndex].correctAnswer] 가 의미하는 것이 무엇인가



먼저, JSON이란?

JavaScript Object Notation으로 속성-값 쌍( attribute–value pairs and array data types (or any other serializable value)) 또는 "키-값 쌍"으로 이루어진 데이터 오브젝트를 전달하기 위해 인간이 읽을 수 있는 텍스트를 사용하는 개방형 표준 포맷이다.

비동기 브라우저/서버 통신 (AJAX)을 위해, 넓게는 XML(AJAX가 사용)을 대체하는 주요 데이터 포맷이다. 특히, 인터넷에서 자료를 주고 받을 때 그 자료를 표현하는 방법으로 알려져 있다. 자료의 종류에 큰 제한은 없으며, 특히 컴퓨터 프로그램의 변수값을 표현하는 데 적합하다.



문제점 1)

마지막 문제만 화면에 보여지고 다음 퀴즈로 넘어가는 형식을 구현하지 못했었음

피드백)

위 함수 외부에서 dataIndex, currentDataIndex와 같은 변수를 선언하고, data라는 quiz 배열의 인덱스로 접근하여 문제를 렌더링하는 방식을 선택할 것 같습니다.
문제를 하나 풀면 위 변수의 값을 1씩 더해가며, 다음 문제를 보여주는 방식으로 보면 될 것 같습니다.

//개선 전 코드 🤔
아예 구현하지 못했고, 어떻게 구현해야될 지도 몰랐었음



//개선 후 코드 ✅
let dataIndex = 0; // ⭐ 맨 밑에 코드에서 다시 재할당 되므로 let선언

nextButton.addEventListener("click", () => { // next버튼을 클릭할 때마다 dataIndex 값이 올라간다.
  dataIndex++ // ⭐ 이 부분을 초점있게 볼 것
  resetQuiz(dataIndex);
  handleStartClick();
});

function resetQuiz(quizIndex) { 
  nextButton.classList.remove("show");
  while (choices.firstChild) {
    choices.removeChild(choices.firstChild)
  }
}

function handleStartClick() { 
  startButton.classList.add("hide");
  totalNumber.classList.remove("hide");
  correctNumber.classList.remove("hide");
  exampleCode.classList.remove("hide");
  showingQuiz();
}

function showingQuiz() {
  question.textContent = data[dataIndex].question;

  if (data[dataIndex].code) {
    exampleCode.innerHTML = `<pre>${data[dataIndex].code}</pre>`;
  }

  if (data[dataIndex].code === null) {
    exampleCode.classList.add("hide");
  }

  totalNumber.textContent = `문제: ${(dataIndex + 1)}  /  ${data.length}`;
  correctNumber.textContent = `맞은 갯수: ${checkQuizCount()}`;

  data[dataIndex].choices.forEach(function(Choice) {
    const quizChoiceDiv = document.createElement("div");
    const quizChoicesP = document.createElement("p");
    let pTagText = document.createTextNode(Choice);

    quizChoicesP.appendChild(pTagText);
    quizChoiceDiv.appendChild(quizChoicesP);
    quizChoiceDiv.classList.add("quiz-choices");
    choices.appendChild(quizChoiceDiv);
  });
}

//어쩌구 저쩌구 구현내용..

if (dataIndex === LAST_QUIZ - 1) {
    checkTotalResults(); // 게임이 끝나고 수고하셨다는 멘트와 함께 맞은 갯수를 보여주는 함수
    dataIndex = 0; // ⭐ 이 부분 초점두고 볼 것
    CorrectAnswerCount = 0; // ⭐ 이 부분도
  }

⛔ 즉, next 버튼을 누를때마다 다음 문제로 넘어가는 방식이므로 nextButton.이벤트리스너 함수에 dataIndex를 1씩 올라가게끔 만든다. 게임을 재시작할때는 원래의 처음 상태 값으로 돌아가야하므로 checkTotalResults() 또는 마지막 퀴즈일때~ 조건문에서 값을 다시 0으로 재할당해준다


문제점 2)

choices에 2개 또는 4개의 답안이 들어 있는데..
이를, 들어 있는 갯수만큼 좀 더 동적으로 선택답지를 보여주지 못했던 점
2개가 아닐 경우 3개 6개와 같은 다른 경우가 생겼을 때 에러가 생길 수 있음

피드백)

외부에서 선언한 dataIndex를 통해 data 배열에 접근 후 choices를 forEach로 순회하면서
choices 내부 element의 수만큼 DOM 요소를 추가하는 함수입니다!

위 방법이 정답은 아니지만, 여러 관점에서 문제를 해결해보는 경험이 될 수 있을 것 같습니다.

//처음 구현했던 코드 ❌
<html>
4개의 div 태그를 생성해서 그 요소들을 스크립트에서 choices로 잡아줬었음

<script>
if (choicesLength === 2) { 
   choices[0].innerHTML = choice[0] ;
   choices[1].innerHTML = choice[1];
   choices[2].style.display = "none";
   choices[3].style.display = "none";
} else { 
    choices[2].style.display = "flex";
    choices[3].style.display = "flex";
    choices[0].innerHTML = choice[0];
    choices[1].innerHTML = choice[1];
    choices[2].innerHTML = choice[2];
    choices[3].innerHTML = choice[3];
};

피드백 1-1)

이전 코드에서 choices에 전체적으로 show라는 className을 추가하신 것으로 확인됩니다. => 아무생각없이 classList가 아닌 name을 설정해줬던 것 같다..
저였다면 classList의 remove, add를 통해 classList에 대해서 동적으로 변화를 줄 것 같습니다.

// choices[2].style.display = "flex"; 와 같은 방식을
// classList를 통해 수정한 코드
// 전체를 잡아줬던 요소도 인덱스로 접근할 수 있음

  if (choicesLength === 2) {
     choices[0].textContent = choice[0] ;
     choices[1].textContent = choice[1];
     choices[2].classList.remove("show");
     choices[3].classList.remove("show");
     choices[2].classList.add("hide");
     choices[3].classList.add("hide");
} else {
     choices[2].classList.add("flex");
     choices[3].classList.add("flex");
     choices[0].textContent = choice[0];
     choices[1].textContent = choice[1];
     choices[2].textContent = choice[2];
     choices[3].textContent = choice[3];
};

피드백 1-2)

더불어 현재의 방식은 어느정도 제약이 있는 방식으로 생각됩니다.
객관식의 보기가 2개 혹은 4개라는 제약이 있으며, 만약 보기가 3개, 6개가 있다면 버그가 발생할 수 있습니다!

저라면 수진님께서 이전에 사용하신 createElement, appendChild를 통해 보다 동적으로 요소들을 추가하는 코드를 작성하실 수 있을거라고 생각합니다.

createElement, appendChild를 사용하신다면 위와 같이 hide, show와 같은 class를 부여하지 않고 필요한 DOM 요소를 추가할 수 있어 발생한 문제도 해결할 수 있을 것 같습니다.**

// 피드백 반영을 하였지만 forEach를 응용하기 전 코드,, 비효율적 🤔

    const choice = document.createElement("div");
    const pTag = document.createElement("p");
    let pTagText = document.createTextNode(quizList[quizListIndex].choices);

    choices.appendChild(choice);
    choice.appendChild(pTag);
    pTag.appendChild(pTagText);
    pTag.textContent = quizList[quizListIndex].choices;
    choice.classList.add("quiz-choices");
    
//여기서 초이시스가 두개면 두개만 보여주고, 그니까 들어있는 값이 있는만큼만 형태로 보여주고싶으나 구현 x



// 피드백 받은 코드 ✅

⭐ data[dataIndex].choices.forEach(function(Choice) {
     const quizChoiceDiv = document.createElement("div");
     const quizChoicesP = document.createElement("p");
     let pTagText = document.createTextNode(Choice);

     quizChoicesP.appendChild(pTagText);
     quizChoiceDiv.appendChild(quizChoicesP);
     quizChoiceDiv.classList.add("quiz-choices");
     choices.appendChild(quizChoiceDiv);
});

일단 이 피드백에서 정말 여러번의 생각을 할 수 있었는데, 그 전까지는 항상 모든 요소를 html로 다 때려박아서 그걸 바탕으로 자바스크립트상에서 코드를 구현했었다
이런 식으로 forEach 메소드까지 활용한다면 안의 값이 들어있는 만큼 효율적으로, 또 동적으로 코드를 짤 수 있다

👉 [해당 요소].forEach(function(네이밍) {
//구현할 코드 어쩌구 저쩌구...
// 근데 여기서는 안에 그 값이 있는 만큼 요소를 동적으로 생성
}



문제점 3)

  • 조건문에서 눌렀을 때 그 choices가 정답인지 확인하는 부분
  • 조건문에서 전에 피드백 받았던 부분
// 전에 조건문을 늘려서 썼던 것을 계속 신경쓰면서 작성

 if (data[dataIndex].code) {
    exampleCode.innerHTML = `<pre>${data[dataIndex].code}</pre>`;
  }
  
// 수정 전
  if (data[dataIndex].code === null) {
    exampleCode.classList.add("hide");
  }
// 수정 후 ✅
if (!data[dataIndex].code) {
    exampleCode.classList.add("hide");
  }

code는 값이 있을 경우, 또는 값이 null 경우로 나뉘어있다.

때문에 값이 있을 경우는 thruty 값이 되고

<조건문에서 false>

  • null;
  • NaN;
  • 0;
  • 빈 문자열( ""또는 ''또는 ``);
  • undefined.

이므로 null은 fals값으로 쓸 수 있어 !data[dataIndex].code로 사용할 수 있다
!data[dataIndex].code = 코드 값이 false일 때, 즉 위와 같은 값일 때.

//  event.target이 누른 choices가 답과 일치하는지 확인하는 조건

  const quizListChoices = data[dataIndex].choices;
  const correctAnswer = [data[dataIndex].correctAnswer];
  
 if (event.target.textContent === quizListChoices[correctAnswer]) 

data[dataIndex].choices[data[dataIndex].correctAnswer] 얘가 가리키는 뜻이 무엇인지 한참 생각했었는데,,

data[dataIndex].choices: 현재 진행 중인 퀴즈 인덱스의 choices를 가리킴
[data[dataIndex].correctAnswer]: 똑같이 현재 진행중인 퀴즈 인덱스의 정답을 가리키는데 이를 배열안에 넣어 놓았음.

그럼 이 correctAnswer 를 배열로 접근 할 수 있는 것이고
또 이를 choices[data[dataIndex].correctAnswer] 즉 choices의 배열로 넣어 놨으므로 data[dataIndex].choices[해당 퀴즈의 옳은 배열 값] 으로 접근이 가능하다
해당 퀴즈의 옳은 배열 값이 곧 답이고 그것을 choices에서 배열로 값을 넣어놓은..



문제점 4)

요소를 없애는 방법을 classList를 이용해 show, hide만 생각했었는데
이런 식으로 해당 요소가 첫번째 자식이 존재하면 계속해서 없애는 방법으로도 접근할 수 있음.

while (choices.firstChild) {
    choices.removeChild(choices.firstChild)
  }


문제점 5)

choices 사이에 있는 여백 클릭 시에도 색칠 되었던 점

// 처음 코드
choices.addEventListener("click", handleCheckAnswer);

// 수정 코드
 data[dataIndex].choices.forEach(function(Choice) {
    const quizChoiceDiv = document.createElement("div");
    let pTagText = document.createTextNode(Choice);

    quizChoiceDiv.appendChild(pTagText);
    quizChoiceDiv.classList.add("quiz-choices");
    choices.appendChild(quizChoiceDiv);
    
 ⛔ quizChoiceDiv.addEventListener("click", handleCheckAnswer);
  });

choices 전체로 잡아주었던 이벤트리스너를 초점을 더 작게해서 div태그로 수정하였더니 버튼 사이에 여백을 클릭해도 색칠되지 않음

이벤트리스너는 이벤트를 걸어주기 위한 초점을 크게 잡는 것이 아닌 최대한 작게, 또는 맞게 거는 것이 좋은 것 같다

남은 문제점) 중복클릭



마지막으로 피드백을 바탕으로 수정한 코드


import data from "./quiz.json";

const startButton = document.querySelector(".start-button");
const nextButton = document.querySelector(".next-button");
const question = document.querySelector(".question");
const choices = document.querySelector(".choices");
const totalNumber = document.querySelector(".total-number");
const correctNumber = document.querySelector(".correct-number");
const exampleCode = document.querySelector(".example-code");

let dataIndex = 0;
let CorrectAnswerCount = 0;
const LAST_QUIZ = 20;

startButton.addEventListener("click", handleStartClick);
nextButton.addEventListener("click", () => {
  dataIndex++
  resetQuiz(dataIndex);
  handleStartClick();
});

function handleStartClick() {
  startButton.classList.add("hide");
  totalNumber.classList.remove("hide");
  correctNumber.classList.remove("hide");
  exampleCode.classList.remove("hide");
  showingQuiz();
}

function showingQuiz() {
  question.textContent = data[dataIndex].question;

  if (data[dataIndex].code) {
    exampleCode.innerHTML = `<pre>${data[dataIndex].code}</pre>`;
  }

 ⛔ if (!data[dataIndex].code) { // 또는 (data[dataIndex].code === null)
    exampleCode.classList.add("hide");
  }

  totalNumber.textContent = `문제: ${(dataIndex + 1)}  /  ${data.length}`;
  correctNumber.textContent = `맞은 갯수: ${checkQuizCount()}`;

  data[dataIndex].choices.forEach(function(Choice) {
    const quizChoiceDiv = document.createElement("div");
    let pTagText = document.createTextNode(Choice);

    quizChoiceDiv.appendChild(pTagText);
    quizChoiceDiv.classList.add("quiz-choices");
    choices.appendChild(quizChoiceDiv);
    
 ⛔ quizChoiceDiv.addEventListener("click", handleCheckAnswer);
  })
}

function handleCheckAnswer(event) {
  const resutTextDiv = document.createElement("div");
  const resultText = document.createElement("p");
  const quizListChoices = data[dataIndex].choices;
  const correctAnswer = [data[dataIndex].correctAnswer];

  choices.appendChild(resutTextDiv);
  resutTextDiv.appendChild(resultText);
  resultText.classList.add("result-text");

  if (event.target.textContent === quizListChoices[correctAnswer]) {
    event.target.classList.add("correct-answer");
    const pTagText = document.createTextNode("정답입니다~! 👏");
    resultText.appendChild(pTagText);
    checkQuizCount(event);
  } else {
    event.target.classList.add('wrong-answer');
    const pTagText = document.createTextNode("틀렸습니다 ❌");
    resultText.appendChild(pTagText);
    findAnswer();
  }
  nextButton.classList.add("show");

  if (dataIndex === LAST_QUIZ) {
    checkTotalResults();
    dataIndex = 0;
    CorrectAnswerCount = 0;
  }
}

function findAnswer() {
  const resultTextDiv = document.createElement("div");
  const resultTextP = document.createElement("p");
  const pTagText = document.createTextNode("");

  choices.appendChild(resultTextDiv);
  resultTextDiv.appendChild(resultTextP);
  resultTextP.appendChild(pTagText);

  pTagText.textContent = `답은 ${data[dataIndex].choices[data[dataIndex].correctAnswer]} 입니다`;
  resultTextDiv.classList.add("check-answer");
}

function checkQuizCount(correctNumber) {
  if (correctNumber) {
    CorrectAnswerCount += 1;
  }
  return CorrectAnswerCount;
}

function resetQuiz(quizIndex) {
  nextButton.classList.remove("show");

  while (choices.firstChild) {
    choices.removeChild(choices.firstChild)
  }
}

function checkTotalResults() {
  nextButton.classList.remove("show");

  window.setTimeout(function() {
    totalNumber.classList.add("hide");
    correctNumber.classList.add("hide");
    question.classList.add("hide");

    while (choices.firstChild) {
      choices.removeChild(choices.firstChild);
    }

    question.classList.remove("hide");
    startButton.classList.remove("hide");
    startButton.textContent = "Restart"
    question.innerHTML = `🎉 수고하셨습니다~! 🎉<br> 모든 문제를 다 푸셨습니다. 👏 <br>게임을 다시 진행 하시겠습니까?`;
  }, 2000);
}

/* 개선하고 싶은 점
- choices에서 event.target => early return문 활용해보기..
- 이벤트 타겟 중복클릭 
- 문제 당 setTimeout 효과
*/

0개의 댓글