값이 들어있는 만큼 동적으로 요소 생성 & 지우기
문제를 다음으로 어떻게 넘어가게 구현하는가?
JSON에 제시되어있던 값들을 어떻게 가져오는가? => data[dataIndex].choices[data[dataIndex].correctAnswer]
가 의미하는 것이 무엇인가
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; // ⭐ 이 부분도
}
문제점 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];
};
이전 코드에서 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];
};
더불어 현재의 방식은 어느정도 제약이 있는 방식으로 생각됩니다.
객관식의 보기가 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은 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 효과
*/