Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

Leeyanggoo

[JS] 퀴즈 이펙트 7-1!! 불러온 json 데이터를 Math로 splice 하자!! 본문

2023/JavaScript

[JS] 퀴즈 이펙트 7-1!! 불러온 json 데이터를 Math로 splice 하자!!

Leeyanggoo 2023. 4. 4. 04:37

 

 

CBT란 컴퓨터를 이용해 시험을 보는 'Computer Based Test'의 약자입니다.

위의 예제처럼 컴퓨터를 통해 시험을 보고 답을 체크한 뒤 평가를 받을 수 있는 형식의 문제 유형을 만들어 봤습니다.

아직 모든 기능 구현이 끝난 건 아니지만 현 상태까지 쓰인 기능에 대해 소개하겠습니다!

 

문제 정보를 json 파일에 담고 불러오자!!

 

json은 "JavaScript Object Notation"의 약자로 자바스크립트 객체 문법으로 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷을 말합니다.

우리는 불러올 문제 데이터를 자바스크립트 객체 문법으로 구조화 시켜서 가져올 수 있습니다.

 

[
    {
        "subject": "웹디자인기능사",
        "question": "색의 3속성 중 사람의 눈이 가장 예민하고 강하게 반응하는 대비는?",
        "correct_answer": "명도대비",
        "incorrect_answers": ["색상대비", "보색대비", "채도대비"],
        "desc": "명도대비:명도가 다른 두 색의 영향에 의해 명도차가 다르게 지각되는 현상. 주위 색에 따라 더욱 밝게 느껴지거나 더욱 어둡게 느껴짐.<br>ex) 흰바탕에 회색 점/검은바탕에 회색 점동시대비 중 가장 예민하게 작용함."
    },{
        "subject": "웹디자인기능사",
        "question": "색의 주목성에 대한 설명으로 옳지 않은 것은?",
        "correct_answer": "명도와 채도가 낮은 색이 주목성이 높다.",
        "incorrect_answers": ["명시도가 높으면 색의 주목성이 높다.", "채도 차이가 클수록 주목성이 높다.", "빨강은 초록보다 주목성이 높다."],
        "desc": "명도와 채도가 낮으면 주목성도 함께 낮아진다."
    }
]

json 파일 내부 데이터 형식

 

json 파일 내부는 이렇게 자바스크립트의 객체 문법으로 데이터가 구성되어 있습니다.

다른 점이라면 속성의 키(key)와 값(value)에 모두 따옴표(")를 사용해서 표현한다는 점입니다.

따라서 json의 데이터를 불러올 땐 우리가 사용할 형식에 맞게 가공하는 과정이 필요합니다.

 

const cbtQuiz = document.querySelector(".cbt__quiz")
const cbtOmr = document.querySelector(".cbt__omr")
const cbtSubmit = document.querySelector(".cbt__submit")
const totalCount = document.querySelector(".cbt__score .total__count")   //전체 문제 수
const lastCount = document.querySelector(".cbt__score .last__count")   //남은 문제 수
const cbtTime = document.querySelector(".cbt__time")

let questionAll = [];   //모든 퀴즈 정보

//데이터 가져오기
const dataQuestion = () => {
    fetch("json/gineungsaWD2023_01.json")
    .then(res => res.json())
    .then(items => {
        questionAll = items.map((item, index) => {
            const formattedQuestion = {
                question: item.question,
                number: index+1,
            };
            const answerChoices = [...item.incorrect_answers];  //오답 불러오기
            formattedQuestion.answer = Math.ceil(Math.random() * (answerChoices.length+1)); //정답을 랜덤으로 불러오기
            answerChoices.splice(formattedQuestion.answer -1, 0, item.correct_answer); //정답을 랜덤으로 추가

            //보기를 추가하기
            answerChoices.forEach((choice, index) => {  
                formattedQuestion["choice" + (index+1)] = choice;
            });

            //문제 해설이 있으면(true) 출력
            if(item.hasOwnProperty("question_desc")){
                formattedQuestion.questionDesc = item.question_desc;
            }

            //문제에 이미지가 있으면 출력
            if(item.hasOwnProperty("question_img")){
                formattedQuestion.questionImg = item.question_img;
            } else {
                formattedQuestion.questionImg = "";
            }

            //해설이 있으면 출력
            if(item.hasOwnProperty("desc")){
                formattedQuestion.desc = item.desc;
            }

            //전체 문제 개수
            totalCount.innerHTML = items.length;
            lastCount.innerHTML = items.length;

            return formattedQuestion;
        });                
        newQuestion();
    })
    .catch((err) => console.log(err));
}

 

먼저 json 파일 속에 있는 데이터를 담기 위해 questionAll이라는 변수를 선언하고 배열을 할당했습니다.

그다음 json을 불러오기 위한 특별한 함수를 실행하게 되는데요, 바로 "fetch()" 함수입니다.

 

fetch() 함수 짚고 넘어가자!

 

fetch() 함수는 자바스크립트에서 주어진 리소스(URL)를 가져오는 데 사용되는 내장 함수입니다.

then() 메서드는 이전 작업의 결과를 받아 다음 작업을 처리하는 데 사용됩니다.

따라서 fetch() 함수가 리소스를 가져오는 데 성공했다면, then() 메서드를 실행하게 됩니다.

첫 번째 then() 메서드에서는 fetch() 함수가 반환한 객체(res)를 자바스크립트 객체로 변환(res.json())합니다. 이를 파싱이라고 합니다.

두 번째 then() 메서드는 파싱된 데이터를 items 인자로 받습니다. 따라서 items 변수는 json 데이터의 내용을 담고 있으며, 이를 이용해 필요한 작업을 수행하게 됩니다.

 

펼침연산자를 활용해 데이터를 다듬자!

 

json 파일 속의 오답(incorrect_answers)은 값이 배열로 되어 있으므로 이를 활용하기 위해선 배열을 전개해서 활용해야 합니다. 이때 사용하는 것이 펼침연산자(Spread Operator)입니다.

펼침연산자는 배열이나 객체 변수명 앞에 마침표 3개(...)를 사용해서 사용합니다. 이번 예제에서는 "const answerChoices = [...item.incorrect_answers];"에 해당합니다.

펼침연산자는 자바스크립트에서 배열, 객체, 문자열 등의 내용을 전개하여 개별 요소로 분리합니다.

이를 통해 함수 호출 시 인수를 전달하거나, 새로운 배열이나 객체를 생성할 수 있습니다.

우리는 4개의 보기를 가진 문제를 만들어야 하기 때문에 개별 요소로 분리하고, 정답을 추가하기 위해 answerChoices라는 변수에 배열을 할당하고 있습니다.

이로써 원본 데이터에 영향을 주지 않고 새로운 배열을 생성할 수 있습니다.

 

Math 객체와 splice() 메서드를 이용해서 랜덤한 보기를 만들자!

 

Math 객체는 수학석 상수와 함수를 위한 속성과 메서드를 제공하는 내장 객체입니다.

Math.random()은 0 이상 1 미만의 난수 값을 반환하는 함수입니다.

Math.ceil(num)은 'num'보다 크거나 같은 가장 작은 정수를 반환합니다. 올림입니다.

Math.floor(num)은 'num'보다 작거나 같은 가장 큰 정수를 반환합니다. 내림입니다.

 

splice() 메서드는 배열의 특정 위치에 요소를 추가하거나 삭제할 수 있는 자바스크립트 메서드입니다.

splice(시작 위치, 삭제 개수, 추가할 요소)로 이뤄집니다.

 

"formattedQuestion.answer = Math.ceil(Math.random() * (answerChoices.length+1))"를 차근차근 이해해보겠습니다.

먼저 Math.ceil() 안부터 계산해 봅시다.

Math.random()은 0과 1 사이의 난수 값이므로 0~0.9999...사이의 값이며, answerChoices.length는 오답 개수인 '3'이 나오게 됩니다.

따라서 Math.random() * answerChoices.length는 0 이상 3 미만의 숫자가 나오게 됩니다.

그리고 우리는 이 계산을 올림 메서드인 Math.ceil()로 묶고 있기 때문에 1 이상 3 이하의 숫자가 나옵니다.

 

이렇게 얻은 무작위 정수들은 정답이 들어갈 배열의 인덱스로 사용합니다.

따라서 배열의 인덱스라면 4개의 보기이기 때문에 0부터 3까지의 숫자가 필요합니다.

하지만 추후에 선택한 정답의 번호와 정답을 맞추기 위해선 1부터 4까지의 숫자를 불러오면서 인덱스 자리에서 1을 빼는 게 제일 효율적일 것입니다.

따라서 answerChoices.length에 1을 미리 더하게 되면, Math.random()의 0 이상 1 미만의 난수 값과 4가 되는 (answerChoices.length+1)의 계산으로 1이상 4이하의 숫자가 나오게 됩니다.

 

이제 배열의 인덱스 값을 구했으니 나머지는 splice() 메서드의 속성 순서에 따라 값을 넣으면 됩니다.

첫 번째는 랜덤한 배열의 시작 위치이기 때문에 0부터 3의 값을 넣고, 제거가 아닌 추가이기 때문에 두 번째는 0을 넣습니다.

세 번째는 json 파일의 정답을 담고 있는 item.correct_answer를 입력함으로써 랜덤한 위치에 정답을 넣게 됩니다.

 

나머지의 데이터 대입은 이전 퀴즈 이펙트에서 다루는 내용이니 참고하시면 좋을 것 같습니다!!

다음엔 다른 기능도 함께 추가해서 완성한 문제집을 올리도록 하겠습니다!

 

//정답 확인
const answerQuiz = () => {
    const cbtSelects = document.querySelectorAll(".cbt__selects");

    questionAll.forEach((question, number) => {
        const quizSelectWrap = cbtSelects[number];
        const userSelector = `input[name=select${number}]:checked`;

        const userAnswer = (quizSelectWrap.querySelector(userSelector) || {}).value;

        const numberAnswer = userAnswer ? userAnswer.slice(-1) : undefined;
        console.log(numberAnswer)

        //사용자의 정답 확인
        if(numberAnswer == question.answer){
            cbtSelects[number].parentElement.classList.add("good");
        } else {
            cbtSelects[number].parentElement.classList.add("bad");

            //오답 체크
            const label = cbtSelects[number].querySelectorAll("label");
            label[question.answer-1].classList.add("correct")
        }

        //설명 추가
        const quizDesc = document.querySelectorAll(".cbt__desc");

        if(quizDesc[number].innerHTML == "undefined"){
            quizDesc[number].classList.add("hide");
        } else {
            quizDesc[number].classList.remove("hide");
        }
    });
};

cbtSubmit.addEventListener("click", answerQuiz);
dataQuestion();       


const answerSelect = () => {
    // 현재 사용자가 푼 문제 수를 계산합니다.
    const answeredCount = document.querySelectorAll('input[type="radio"]:checked').length;
    // 남은 문제 수를 계산합니다.
    const remainingCount = questionAll.length - answeredCount;
    // 남은 문제 수를 `lastCount` 요소에 표시합니다.
    lastCount.innerText = remainingCount;
    // 남은 문제 수를 반환합니다.
    return remainingCount;
};

 


 

😮‍💨 이전 포스팅 보러 가기

😮‍💨 이번 예제 보러 가기

😮‍💨 더 다양한 퀴즈 이펙트 보러 가기