3주차 과제 Review

1. 요구 사항

✳️ TODO

1️⃣ 퀴즈를 시작할 수 있는 버튼이 보여야 합니다.
2️⃣ 사용자가 시작 버튼을 눌렀을 경우, quiz.json의 첫 문제부터 카드 형식으로 보여주어야 합니다.
3️⃣ 사용자가 현재까지 맞춘 정답의 갯수와 남은 문제의 숫자를 화면에 표기해주어야 합니다.
4️⃣ 문제에 따라 예제 코드가 있다면, 함께 보여주어야 합니다.
5️⃣ 문제의 정답을 맞출 경우, 정답이라고 표기해주고 다음 문제로 진행할 수 있는 버튼이 나타나야 합니다.
6️⃣ 문제의 정답을 맞추지 못했을 경우, 정답을 표기해주고 다음 문제로 진행할 수 있는 버튼이 나타나야 합니다.
7️⃣ 마지막 문제가 끝났을 경우, 수고했다는 메시지와 함께 재시작 버튼이 나타나야 합니다.
8️⃣ 재시작 버튼을 눌렀을 경우, 다시 처음부터 위 단계를 반복할 수 있어야 합니다.

✳️ Advanced

1️⃣ 한 문제에 대한 제한 시간을 두고, 제한 시간 내에 정답을 맞추지 못했을 경우 오답 처리하는 기능을 추가해보세요.

일단 모두 충족시키긴 함.


2. 개선해야 할 사항

1) Javascript

1️⃣ 정답의 개수를 나타내는 변수명으로 number가 앞으로 온다면numberOfCorrectAnswers 처럼 중간에 of가 있어야 자연스러운 것 같다.

혹은 number라는 단어를 뒤로 보낼 수 있다.
하지만 전자(numberOfCorrectAnswers)나 후자(correctAnswersNumber)의 경우 둘 다 정답의 개수가 아닌 정답이 몇 번인지 나타내는 변수명 같음.

참고로 개수를 나타내는 단어론 count를 많이 사용하기도 한다.

// 개선 전
let numberCorrectAnswers = 0;

➡️
// 개선 후
let CountOfCorrectAnswers = 0;

2️⃣ HTML 태그를 직접 작성해주는 것 보다 DOM을 생성한 후 자식 요소로 추가해 주는게 DOM을 조금 더 동적으로 제어 할 수 있는 방법이다.

// 개선 전
 QuestionElement.innerHTML = `<p>${data[quizNumber].question}</p>`;
 
 ➡️
 // 개선 후
 const question = document.createElement('p'); // DOM을 생성합니다.
  question.classList.add('question'); // DOM에 클래스를 부여해줍니다.
  question.textContent = data[quizNumber].question;

  questionElement.append(question); // p 태그로 생성된 DOM을 questionElement의 자식 요소로 추가해줍니다.

3️⃣ if-else 구문이 코드에 많다면 가독성에 좋지 않다.

// 개선 전
function limitTime() {
  timeLimit -= 1;
  if (timeLimit >= 0) {
    timerElement.innerHTML = `남은 시간 : ${timeLimit}`;
  } else {
    commentElement.innerHTML = `시간초과...`;
    findRightAnswer();
    processAfterCheckAnswer();
  }

위와 같은 코드를

if-else구문을 좀 더 깔끔하게 사용하기 위해 early return 하는 코드로 변경할 수 있다.
if 조건문에 들어오면 해당하는 동작을 하고 return을 하는 방법.

early return으로 코드를 작성할 시 장점

  1. return문을 작성해줌으로써 "이 함수에서 더 이상의 동작은 없다." 라는 뜻을 명확하게 전달해 줄 수 있다.
  2. 들여쓰기(인덴팅) 깊이가 줄어 코드를 더욱 읽기 쉽게 만들어 준다. (가독성 👍)
  3. 조건문 아래쪽에서 의도하지 않은 값의 변화를 방지할 수 있다.
// 개선 후
  if (timeLimit === 0) {
    commentElement.innerHTML = '시간초과...';
    findRightAnswer();
    processAfterCheckAnswer();
    return;
  }

  timerElement.innerHTML = `남은 시간 : ${timeLimit}`;

이렇게 early return으로 바꿀 수 있다.


4️⃣ 문자열에 값이 들어가 표현식이 필요한 경우에는템플릿 리터럴(Template leterals) 를 사용할 수 있다.

템플릿 리터럴은 따옴표 대신 백틱을 사용.

하지만 현재는 표현식을 넣고 있지 않기 때문에 백틱을 이용하는 것 보다는 다른 곳과 통일감 있게 따옴표를 사용해 문자열을 표기하는게 더 자연스럽다.

// 개선 전
commentElement.innerHTML = `정답입니다~~!`;

➡️
// 개선 후
commentElement.innerHTML = '정답입니다~~!';

위와 같이 표현식이 없는 경우 통일감있게 따옴표 사용


5️⃣ 연산자의 양 옆으론 띄어쓰기를 줘 가독성을 더 좋게 해줄 수 있다.

// 개선 전
if (quizNumber !== data.length-1) {
    
➡️
    
//개선 후
if (quizNumber !== data.length - 1) {

6️⃣ 반복문을 작성할 때 forEach 메서드를 사용할 수도 있다.

가독성 측면도 있지만 forEach를 사용하면 루프를 돌 때 마다 다른 스코프를 형성하기 때문에 의도하지 않은 오류를 방지할 수 있다는 장점 또한 있다.

(airbnb 가이드에서도 이터레이터 사용보다 메서드 사용 방식을 권장하기도 함.)

    for (let i = 0; i < choiceElements.length; i++) {
      choiceElements[i].removeEventListener('click', handleCheckAnswer);
    }

➡️

    choicesElements.forEach((element) => {
      element.removeEventListener('click', handleCheckAnswer);
    });

7️⃣ setInterval지연 간격을 보장하지 않는다.

이를 해결하기 위한 방법으로 setTimeout을 중첩해 사용 할 수 있다.

중첩 setTimeout을 사용하면 setInterval을 사용한 것보다 유연하게 코드를 작성할 수 있으며 setInterval이 보장하지 않는 지연 시간 간격 또한 보장한다는 장점이 있다.

참고 사이트 : 중첩 setTimeout


8️⃣ 조건이 길어질 경우 어떤 조건에 내부 로직이 실행되는지 가늠하기 어려울 수 있다.

이럴 경우 따로 변수를 선언해줘 어떤 경우인지 명시적으로 보여주는 방법도 있다.

function findRightAnswer() {
  for (let j = 0; j < data[quizNumber].choices.length; j++) {
    if (choiceElements[j].textContent === data[quizNumber].choices[data[quizNumber].correctAnswer]) {
      
   ➡️
      
const quizChoices = data[quizNumber].choices;
const answerIndex = data[quizNumber].correctAnswer;

for (let j = 0; j < quizChoices.length; j++) {
    if (choiceElements[j].textContent === quizChoices[answerIndex]) {

// 혹은 구조 분해 할당(디스트럭처링 Destructuring)을 사용해 따로 변수를 선언하지 않고 객체의 속성을 해체해 개별 변수처럼 사용 할 수 있다.
      
  const { choices, correctAnswer } = data[quizNumber];

  for (let j = 0; j < quizChoices.length; j++) {
    if (choiceElements[j].textContent === choices[correctAnswer]) {
     

참고 사이트 : 구조 분해 할당


9️⃣ 이미 for문을 돌고 있기 때문에 choiceElement에 바로 이벤트 리스너를 줄 수 있다.

  for (let j = 0; j < data[quizNumber].choices.length; j++) {
    const choiceElement = document.createElement('li');
    choiceElement.classList.add('main-section__choice');
    choiceElement.textContent = `${data[quizNumber].choices[j]}`;
    choicesElement.appendChild(choiceElement);
  }
  
  choiceElements = document.querySelectorAll('.main-section__choice');
  for (let i = 0; i < choiceElements.length; i++) {
    choiceElements[i].addEventListener('click', handleCheckAnswer);
  }
  
  ➡️
  
  for (let j = 0; j < data[quizNumber].choices.length; j++) {
    const choiceElement = document.createElement('li');
    choiceElement.classList.add('main-section__choice');
    choiceElement.textContent = `${data[quizNumber].choices[j]}`;
    choicesElement.appendChild(choiceElement);

    choiceElement.addEventListener('click', handleCheckAnswer);
  }

🔟 정답은 아니지만 DOM 요소를 셀렉터를 이용해 변수로 선언하는 경우 prefix(접두사)로 $ 달러 사인을 붙일 수 있습니다.

이미 존재하는 DOM요소일 경우, 일반 변수일 경우, 자바스크립트를 이용해 동적으로 생성한 DOM일 경우가 명확하게 구분되 선호하는 방법.

const timerElement = document.querySelector('.main-section__timer');

➡️

const $timer = document.querySelector('.main-section__timer');

🔟1️⃣ setInterval 함수의 첫번째 인자로 들어가는 함수의 이름으로 '시간을 제한하다' 혹은 '제한 시간' 이라는 명칭은 잘 어울리지 않는다.

시간을 1초씩 감소시키고 남은 시간을 표기하는 함수이니 countTime 정도의 이름이 더 명확한 것 같다.

function limitTime() {

➡️

function countTime() {

🔟2️⃣ 함수는 동사형으로 작성한다.

현재 함수명은 "정답을 체크 한 후의 과정" 라는 뜻의 명사 느낌이다.

또한 어떤 과정인지 명시적으로 함수명에 작성을 해주어야, 정답을 체크한 후 어떤 일들이 일어나는지 굳이 내용을 보지 않고 이름으로도 이해할 수 있다.

(만약 내부에서 수행하고 있는 일들이 많아 함수명을 작성하기가 어려운 경우라면, 이 로직이 꼭 이 함수 안으로 들어가야하는지 내부 로직을 한번 더 검토하거나 함수를 분리시키는 것도 좋은 방법중에 하나입니다.)

남은 문제 갯수를 카운팅해 사용자에게 보여지게하고, 마지막 문제인지 아닌지 체크하는 로직이기 때문에 checkQuizCount, countQuizNumber 정도의 이름으로 작성할 수 있을 것 같다.

function processAfterCheckAnswer() {
  
➡️

function checkQuizCount() {

🔟3️⃣ 변수명은 camelCase로 작성한다.


🔟4️⃣ 현재 if-else 구문 둘 다 같은 for문이 존재하고 있어 로직이 복잡해 보일 수 있다.

for문은 quizNumber가 마지막이거나 마지막이 아닐 때 두 조건 모두 적용되어야 하는 로직이기 때문에 밖으로 뺀다면 조금 더 간결해 질 것 같다.

더불어 상단에서 알려준 early return을 활용해 리팩토링을 할 수 있을 것 같다.

  if (quizNumber !== data.length-1) {
    nextButtonElement.classList.remove(INVISIBLE);
    for (let i = 0; i < choiceElements.length; i++) {
      choiceElements[i].removeEventListener('click', handleCheckAnswer);
    }
  } else {
    for (let i = 0; i < choiceElements.length; i++) {
      choiceElements[i].removeEventListener('click', handleCheckAnswer);
    }
    restartButtonElement.classList.remove(INVISIBLE);
    lastCommentElement.classList.remove(INVISIBLE);
    restartButtonElement.addEventListener('click', handleRestartQuiz);
  }
  quizNumber += 1;
  nextButtonElement.addEventListener('click', showNextQuiz);
  clearInterval(intervalLimitTime);
}

➡️

  choiceElements.forEach((choice) => {
    choice.removeEventListener('click', handleCheckAnswer);
  });

  if (quizNumber === data.length - 1) {
    restartButtonElement.classList.remove(INVISIBLE);
    lastCommentElement.classList.remove(INVISIBLE);
    restartButtonElement.addEventListener('click', handleRestartQuiz);
    clearInterval(timer);
    return;
  }

  quizNumber += 1;
  nextButtonElement.classList.remove(INVISIBLE);
  nextButtonElement.addEventListener('click', showNextQuiz);
  clearInterval(timer);
}

2) CSS

1️⃣ data에 있는 code에 줄바꿈을 할 수 있게 이스케이프 문자(\n) 가 사용되고 있다.

하지만 현재는 줄바꿈이 필요한 위치에 개행이 되고 있지 않다.
이럴 경우 CSS에서 공백 문자를 처리할 수 있는 white-space 를 이용해 개행을 해줄 수 있다.

참고 사이트 : white-space


2️⃣ 색깔을 클래스명으로 작성하는 것보다는 해당 색깔이 어디서 어떤 역할을 하는지 작성해주는게 더 명확한 것 같다.

.blue {
  background-color: rgb(85, 85, 219);
}

.red {
  background-color: rgb(224, 94, 94);
}

3️⃣ 페이지 진입시 나오는 첫 화면을 랜딩페이지(Landing Page) 라고 칭하기도 한다.

.first-page {

➡️

.landing-page {

4️⃣ 아래와 같이 표기 가능하다.

  background-color: rgba(0, 0, 0, 0);
  
  ➡️
  
  background: none;

3. 잘 한 부분

1️⃣ 상수처리 잘 함! 👍

const startButtonElement = document.querySelector('.start-button');
const firstPageElement = document.querySelector('.first-page');
const mainSectionElement = document.querySelector('.main-section');
const QuestionElement = document.querySelector('.main-section__question');
const exampleCodeElement = document.querySelector('.main-section__code');
const CorrectAnswersElement = document.querySelector('.main-section__number-answers');
const RemainingQuizElement = document.querySelector('.main-section__number-remaining');
const choicesElement = document.querySelector('.main-section__choices');
const commentElement = document.querySelector('.main-section__comment');
const nextButtonElement = document.querySelector('.main-section__next-button');
const restartButtonElement = document.querySelector('.restart-button');
const lastCommentElement = document.querySelector('.last-comment');
const timerElement = document.querySelector('.main-section__timer');

const INVISIBLE = 'invisible';
const TIME_LIMIT = 20;

4. 추가적인 공부가 필요한 부분

1️⃣ 텍스트를 추가할 수 있는 방법중에 innerHTML, innerText보다 성능이 더 좋은 textContent도 있다.

셋의 차이가 무엇이고 각 특성에 대해 꼭 공부해보길 바란다.

참고 사이트 : innerText vs innerHTML vs TextContent
당신이 innerHTML을 쓰면 안되는 이유

2️⃣ 요소를 보이지 않게 하기 위해 보통 세 가지 CSS 속성을 줄 수 있다.

  1. display: none;
  2. visibility: hidden;
  3. opacity: 0;

세 속성 모두 요소를 보이거나 보이지 않게 하지만 영역이 남아있는지, 클릭이 가능한지에 따라 특징이 다르다.

자세한 특징과 차이에 대해 꼭 알아보시길 바람~

참고 사이트 : display: none, visibility : hidden, opacity : 0

3️⃣

5. 멘토님의 평가

안녕하세요 진권님! 이번 코드리뷰를 맡게 된 김도희입니다.
타이머 기능까지 모두 잘 구현해주셨네요! 정말 수고하셨습니다. 👏👏👏
함수명이나 코드 포맷팅을 꼼꼼하게 신경써서 작업하신 노력의 흔적이 보여 좋았습니다.
더불어 질문 주신 점은 리뷰에 답변해드렸으니 꼭 읽어보시고 리팩토링 하시면 좋을 것 같습니다.
그럼 다음주도 화이팅입니다!

정말 감사드립니다!

참고 : innerText vs innerHTML vs TextContent
display: none, visibility : hidden, opacity : 0

좋은 웹페이지 즐겨찾기