[공통 & 핵심 피드백] 3주차 Quiz

⭐ choices에 들어있는 값 만큼 동적으로 어떻게 생성했나?

choice는 주어지는 데이터에 따라 2개에서 4개로 보여지는데, 이 부분은 html에서 다루기 보다는
항상 달라지는 동적인 데이터이기 때문에 자바스크립트 파일 내에서 다루는것이 옳다.

// for문을 이용한 동적인 요소 생성 방법

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);
  }


⭐ choices 답안을 없애는 또 다른 방법

  • forEach를 이용
  • for를 이용
  • 그냥 전체 요소에서 textContent 값을 "" 빈 객체로 지정
// 내가 썼던 방법과 동일 forEach와 while문에서 removeChild를 이용 ⛔
toReset.forEach((parent) => {
    while (parent.hasChildNodes()) {
      parent.removeChild(parent.lastChild); ⭐
    }
  });
  
// 이를 이렇게 textContent의 값을 없애는 방법으로 정리해줄 수 있음 ✅
// 이 방법이 가장 심플한 방법인 것 같다
  toReset.forEach((parent) => {
    parent.textContent = ""; ⭐
  });

⛔ 위에서 처음 작성한 코드처럼 while으로 순회할 경우 의도치 않게 무한루프에 빠질 수 있으므로 다른 방식으로 지우는 것을 추천

"이전 문제를 지운다"는 역할을 하는 작은 함수로 선언하여 역할을 분리하는 방법도 있습니다.
위 내용을 반영하면 아래와 같습니다. ( 아래에선 작은 함수를 선언하여 remove로 지워줌)

function deleteCurrentChoiceList () {
  for (let i = 0; i < $ChoiceList.length; i++) {
    $ChoiceList[i].remove(); ✅
  }
}

function handleNextButton () {
  index++;
  deleteCurrentChoiceList();
  ...
}

👉 즉, 보통 동적인 요소 (선택 답지) 를 다시 없앨 때 remove() 또는 textContent = ""; 를 이용하여 요소를 없애 주었다



timer 부분

추후 보충 필요..



내가 막혔던 부분과 같은 event.target 부분

event.target현재 이벤트가 발생된 element를 말합니다.
비슷해보이는 event.currentTarget은 이벤트가 실제로 추가 되어있는 element입니다.

즉, 가장 상위의 element에 이벤트 함수를 추가해두고,
원하는 element를 클릭했을때만 원하는 기능을 구현할 수 있습니다.

<ul>
 <li></li>
 <li></li>
 <li>
   <div></div>
 </li>
</ul>
const ul = document.querySelector("ul");
ul.addEventListener("click", handleClickUl);

function handleClickUl(event) {
  if (/* event.target이 li elment가 아니면? */) return;
  // 원하는 기능 구현...
}

위에 설명한 내용이 가장 기본적인 이벤트 위임(event delegation)의 개념입니다.



is, has

비교문의 가독성을 높이려면
아래와 같이 is~, has~등의 사용을 통해 가독성을 높여줄 수 있습니다.
아래는 예시이기 때문에 참고만 해주세요.

 if (statusArr[count].choice !== null) {
 
 // 아래와 같이 수정 ✅
  const hasPreviousSelection = statusArr[count].choice !== null;
  if (hasPreviousSelection) {
    ...
  }


eventListener

이벤트리스너 정리해주기

항상 작동하는 이벤트 리스너가 아니라면 최대한 이벤트리스너를 정리해주는것이 좋다
의도하지 않게 이벤트가 두번 달릴수도 있고 사용되지 않는 이벤트 리스너이기 때문에 메모리에 낭비가 생길 수 있습니다.

startButton.addEventListener('click', handleStartClick) =>
startButton.removeEventListener('click', handleStartClick)처럼 정리해주기



eventListener 자체에서 정의되는 함수는 익명함수이기 때문에 화살표 함수로도 표현할 수 있다.

startButton.addEventListener("click", function () {

//수정 후 ✅
startButton.addEventListener("click", () => {


또 이런식으로도 이벤트리스너를 활용할 수 있음

element.addEventListener("click", (ev) => {
  if (ev.target.className === "...") { ✅
    /// 원하는 로직 실행
  }
}

자식 노드의 class 이름(event.target.className), 혹은 다른 정보들 (event.target으로 판별 가능한)
을 통해 조건문을 걸어주는 방식

자바스크립트 상에서 요소를 잡아줄 때 클래스이름을 사용할 것을 권함
왜나면, ol이런식으로 태그로 잡아주게 된다면 나중에 요소를 추가적으로 넣어줄때 같이 잡히는 불상사가..






중복되는 classList 명

아래 코드에서 "hidden" 스트링 리터럴이 굉장히 많이 쓰이고 있습니다.
이럴땐 이 "hidden" 스트링 리터럴을 따로 상수화 시키는 것이 좋습니다


$startButton.classList.add("hidden");
    
// 수정 후 ✅
const CLASSNAME_HIDDEN = "hidden
$code.classList.add(CLASSNAME_HIDDEN);

⭐ 이렇게 할 경우에는 다음과 같은 장점이 있습니다.


스트링 리터럴보다 한눈에 알아보기 쉽다. (스트링 리터럴이 길고 복잡할수록 기대치가 커집니다.)

이 스트링 리터럴 값이 바뀌어야될 때 예로, hidden -> unvisible과 같이 바뀌어야 될 경우 모든 스트링 리터럴을 수정하는 것이아닌, 상수만 수정하면 일괄적용시킬 수 있습니다. (중복의 최소화)

반복적으로 사용되는 클래스명은 상수 처리해주시면 좋습니다.
상수로 처리하면 추후 일괄적으로 값을 바꿀 때도 편리하고 오타와 같은 실수도 방지할 수 있습니다.

 startButtonContainer.classList.add("hide");
 
 // 수정 후 ✅
  const HIDE_CLASS = 'hide';
  startButtonContainer.classList.add(HIDE_CLASS);


사용하지 않는 인자 네이밍

인자로 넘기는 콜백함수도 마찬가지로 네이밍이 정확할수록 좋습니다.
사용하지 않는 인자는 _ 로 표현하기도 합니다.

// 수정 전
choicesContainerElement.forEach((v, i) => choicesContainerElement[i].style.visibility = 'hidden');

//수정 후 ✅
  choicesContainerElement.forEach((_, i) => choicesContainerElement[i].style.visibility = 'hidden');
// 수정 전
 choicesContainerElement.forEach((v, i) => choicesContainerElement[i].style.visibility = 'hidden');
 
 //수정 후 ✅
  choicesContainerElement.forEach((element) => element.style.visibility = 'hidden');


중복되는 부분 함수화, 함수 재활용성↑

아래 코드에서 패턴이 계속 반복되는 것을 확인할 수 있습니다.

Element.classList.add("hidden") Element.classList.remove("hidden")
이를 모든 Element에 대한 add, remove 처리를 해도 무리는 없으나, 하나의 함수화 할 수 있다면 좀 더 가독성이 좋은 코드가 될 것 같습니다.

function setClassName($element, className, flag = true) {
  if (flag) {
    $element.classList.add(className);
  } else {
    $element.classList.remove(className);
  }
}

사용은 아래와 같이. ✅

$code.classList.remove("hidden");
		setClassName($code, CLASSNAME_HIDDEN, false);
// 수정 전 ⛔
function makeQuestion(idx) {
  return $quizQuestion.textContent = data[idx].question;
}
// 수정 후 ✅
function setTextContent(element, text) {
  element.textContent = text;
}


전위, 후위 연산자

사람에 따라, 상황에 따라 다르지만 특별히 쓸 상황이 아니라면,
correctCount++; => correctCount += 1; 과 같이
변수에 할당하는 느낌이 나도록 하는걸 선호하는 편 하지만 상황에 따라 다를 수 있음.
그 순간에 가독성에 좀 더 초점을 맞춰서 사용할 것


변수 선언 시 초기화 값 할당

let timer; // 초기 값 없을 때 ❌
let timer = null; 초기 값이 없음을 암시하는 null을 설정 ✅


⭐ 지역변수와 전역변수

함수내에서 쓰이기만하는 변수(로컬변수)라면 무조건 함수내부에 선언이 되어야 한다고 생각합니다.


그 이유는,
1) 전역변수로 선언하게되면 전역변수로 선언해둔 그 변수를 찾으러 돌아다녀야된다는 점
2) 전역변수로 선언할시에 불필요한 변수가 계속 남아있게 된다는 점

함수 내부에서만 쓰는 변수인데 굳이 전역으로 선언해서 이제 더이상 쓰지 않는데 javascript context 상에 남아있게 되는 것이죠.
그래서 함수내부에서만 쓰이는 변수라면 무조건 함수내부에서 선언하는 것이 맞습니다.

계속 여러번 참조해야하는 값이 아니라면 전역변수는 최대한 사용을 지양하는 것이 좋음



전역변수의 네이밍과 매개변수의 네이밍

저는 전역에 변수가 존재한다면, 같은 scope내에 있는 함수의 매개변수명을 동일하게 주는 것은 피하려고 합니다.
함수의 내용이 길어졌을때, 내부에서 변수를 사용하는 경우
해당 변수가 매개변수인지 전역변수인지를 다시 한번 생각해봐야 한다는 점 때문입니다.
eslint규칙으로는 no-shadow라고 있습니다.

한 글자 혹은 아무런 의미가 전달되지 않는 네이밍은 지양해야합니다.
네이밍을 할 때는 항상 역할과 무엇을 담고있는 것인지 다른 사람이 알아차릴 수 있게.



display:none과 visibility:hidden 차이

👉 https://mber.tistory.com/42



상수 값

지금은 퀴즈의 개수가 21개라서 문제가 없지만, 만약 퀴즈의 개수가 늘어나거나 줄어들면 코드도 매번 수정이 필요하겠죠? 🤔
매번 수정할 필요없이 data의 길이를 통해서 동적으로 받아올 수 있게 수정해보시면 좋을 것 같습니다.

❌ questionsLeft.textContent = 21;
 
✅ questionsLeft.textContent = data.length;

또, 일반적으로 상수 같은 경우 값이 변동이 없이 사용되는 것을 말합니다.

하지만, 밑에 예시와 같이 유저가 각 문제에서 선택한 선택지를 담고 있는 배열처럼 변동이 있는 요소는
일반적인 변수 네이밍소문자 + camelCase 사용을 추천드립니다!

⛔ const USER_ANSWERS = Array(QUESTIONS_COUNT).fill(null);
✅ const userAnswers = Array(QUESTIONS_COUNT).fill(null);


삼항연산자

// 삼항 연산자 예제
  code.textContent = data[i].code !== null
    ? `예제 : ${data[i].code}`
    : null;
// 수정 전
 if (choiceID === correctAnswer) {
    if (arrIndex < lastIndex) {
      nextBtnElement.innerText = "⭕️Correct! To the next quiz!";
    } else {
      nextBtnElement.innerText = "⭕️Correct! Let's see the result!";
    }
    quizScore.innerText = `Quiz Score: ${currentScore + SCORE_KEY}`;
  } else if (choiceID !== correctAnswer) {
    if (arrIndex < lastIndex) {
      nextBtnElement.innerText = "❌Beep! To the next quiz!";
    } else {
      nextBtnElement.innerText = "❌Beep! Let's see the result!";
    } 
  }
  
  
  // 수정 후 ✅
  let message = "";
  if (choiceID === correctAnswer) {
    message = (arrIndex < lastIndex)
      ? "⭕️Correct! To the next quiz!"
      : "⭕️Correct! Let's see the result!";

    quizScore.innerText = `Quiz Score: ${currentScore + SCORE_KEY}`;
  } else {
    message = (arrIndex < lastIndex)
      ? "❌Beep! To the next quiz!"
      : "❌Beep! Let's see the result!";
  }

이런식으로 뛰어서 사용도 가능한가보군..



데이터 속성이란?



두 종류의 비교연산자

Javascript에는 두 종류의 비교연산자가 있습니다.
2개의 연산자를 사용하는 Loose Equality(==, !=)3개의 연산자를 사용하는 Strict Equality(===, !==)가 있습니다.

전자는 두 값의 타입이 다르더라도 형 변환을 통해 같은 값으로 취급할 수 있다면 true로 처리하지만, 후자는 타입이 다르면 false로 처리됩니다.
예를 들어 0 == false는 true, 0 === false는 false가 나오게 됩니다.
일반적으로 Loose Equality는 형 변환에 의해 결과를 예측하기 어렵기 때문에 Strict Equality의 사용을 권장합니다.


⭐ early return

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

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

  1. return문을 작성해줌으로써 "이 함수에서 더 이상의 동작은 없다." 라는 뜻을 명확하게 전달해 줄 수 있습니다.
  2. 들여쓰기(인덴팅) 깊이가 줄어 코드를 더욱 읽기 쉽게 만들어줍니다.
  3. 조건문 아래쪽에서 의도하지 않은 값의 변화를 방지할 수 있습니다.

예제 1)
if (condition1) { ⛔
  // condition1에 해당하는 코드
} else {
  // condition1에 해당하지 않는 코드
}



// 수정 후 ✅
if (condition1) {
  // condition1에 해당하는 코드
  return;
} 

// condition1에 해당하지 않는 그 외의 코드들
예제 2)
// 원래 코드
function takeCareOfTimeOver() {
⛔  if (timerSeconds <= 0) {
    clearInterval(timer);
    showNextButton(CLASSNAME_HIDDEN);

    $quizChoicesContainer.childNodes.forEach(element => {
      element.style = "pointer-events: none";
    });
  }
}
// 피드백
function takeCareOfTimeOver() {
✅  if (timerSeconds > 0) return;

  clearInterval(timer);
  showNextButton();

  $quizChoicesContainer.childNodes.forEach(element => {
    element.style = "pointer-events: none";
  });
}

코드에서 알 수 있듯이 early return은 return을 바로 실행시켜준 다음, 그 밑에 실행할 코드를 적어주는 방식이다

// 활용 전 ❌
function paintTime() { 
    if (timeLimit > ZERO_KEY) {
      quizTimeLimit.innerText = `Quiz Time Limit: ${timeLimit - 1}`;
      timeLimit--;
    } else {
      return;
    }
  }
}
// 활용 ✅
  function paintTime() {
    if (timeLimit <= ZERO_KEY) {
      return;
    }

    quizTimeLimit.innerText = `Quiz Time Limit: ${timeLimit - 1}`;
    timeLimit--;
  }
}


조건이 많으면, switch문 이용

 if (keyCode === 39) {
    handleShowNext();
  } else if (keyCode === 13) {
    handleStartQuiz();
  } else if (keyCode === 37) {
    handleShowBefore();
  } else if (keyCode === 49) {
    exampleLines$[0].click();
  } else if (keyCode === 50) {
    exampleLines$[1].click();
  } else if (keyCode === 51) {
    exampleLines$[2].click();
  } else if (keyCode === 52) {
    exampleLines$[3].click();
  }
}
// 위에 코드를 아래와 같이 사용하는 것이 가독성이 더 좋음 ✅
const keyCode = event.keyCode;

switch (keyCode) {
  case 39:
    handleShowNext();
    break;
  case 13:
    handleStartQuiz();
    break;
  ...
  default:
    break;
}

중첩 if문에서 첫번째 if 후에 오는 if 문에는 중괄호를 생략할 수 있나?



img alt

img tag에서 alt는 필수요소는 아니지만, 접근성 차원에서 매우 중요합니다.



시멘틱 태그

이번 과제에서 wrapper로 div태그를 이용해서 사용하는 사람들이 많았는데 이는 지양하는 것이 좋음

또, text를 작성하는 html 태그로 의미 없는 태그들도 많이 사용하였는데 이보다는 text 태그의 역할로 지정된 것들을 역할에 맞게 사용하는 것이 좋음

content의 의미를 정확하게 설명할 수 있는 tag를 사용하여 시멘틱한 구조를 구현하는 것도 추천합니다.
( 예를 들어 h1, p 모두 text element이지만, "제목"을 나타내는 부분에 p tag를 쓰면 시멘틱한 구조가 아닙니다. )

⛔⛔ 또 공통적으로 p태그를 그냥 띄어쓰기 형태로 쓰는 사람이 많은데, p태그는 문단의 맥락을 나눠주는 의미를 가진 태그이므로 목적에 맞게 사용할 것을 권함.



list tag

목록을 표현할 땐 ul or ol + li tag의 조합으로도 사용



⭐ appendChild를 맨마지막에 작성하는 이유

appendChild는 reflow 현상이 발생하기 때문에 렌더링 최적화 관점에서 관련된 요소를 다 변경하고 나서 마지막에 해주는 것이 좋습니다.
(일반적으로 리플로우를 최소화 하기 위해서 element 생성 -> element에 다양한 변화 줌 -> 부모 element에 넣어주기 순으로 작업합니다.)

추가로 덧붙이면,
hidden class의 설정인 diplay: hidden 역시 reflow 현상이 발생하고,
눈으로 보이는 현상이 같은 visibility: hidden은 repaint 현상이 발생합니다.

diplay: hidden는 단순히 숨겨지는 것에만 끝나지 않고 dom에서도 사라지므로 layout의 재계산이 필요해 reflow가 발생하지만,
visibility: hidden은 단순히 숨겨지므로 layout에 영향이 없어서 repaint가 발생됩니다.
아직은 어려운 개념일 수 있으니 이런 개념이 있다 정도로 인지하시고 넘어가셔도 됩니다.
(여유가 되신다면 reflow, repaint는 중요한 개념인 만큼 검색해보시길 추천합니다.)



li 네이밍

list라는 네이밍은 복수의 형태에서 사용합니다.



setInterval 지워주기

  • 위에서 언급했던 removeEventListener처럼 한번 동작 후 계속해서 사용하지 않는 이벤트는 없애주어야 한다.
    setInterval과 같은 타이머 메소드도 clearInnteval로 삭제해주지 않으면 계속 실행되기 때문에 메모리 누수로 이어질 수 있으므로 주의하기



코드를 표현하는 태그 < 띄어쓰기 >

코드를 표현하는 태그로는 code, pre 등의 태그가 있으니 한 번 찾아보시면 좋을 것 같다는 피드백

self-closing tag, 닫힘 태그없이 혼자 단독 사용하는 태그

<br> tag는 self-closing tag이다.
self-closing tag를 사용할 때는 /와 함께 사용하는 것이 가독성 측면에서 우수합니다

밑에 코드에서 br 태그에 주목할 것

<p class="quiz-replay__text">자바스크립트 지식이 +1 되었습니다.<br>수고하셨습니다.</p>

 //수정 후 ✅
<p class="quiz-replay__text">자바스크립트 지식이 +1 되었습니다.<br />수고하셨습니다.</p>


selecter를 여러 개 같이 작성할 때

selector가 여러 개일 때는 작성하신 것처럼 new line으로 구분하여 작성합니다.
다만 2개씩 묶어서(?) 작성하지 않고 하나의 selector를 new line으로 구분합니다.

❌
.quiz-replay .quiz-firstTime__button, 
.quiz-replay .quiz-replay__button{


//수정 후 ✅
.quiz-replay
.quiz-firstTime__button,
.quiz-replay
.quiz-replay__button {



공통적인 피드백 & 핵심 내용

기본적인 코드스타일 지적이 가장 많았음

  • 세미콜론, 인덴팅, 매직넘버, 논리연산자를 이용한 간결한 조건문, css 프로퍼티 순서 등

이 부분에 대해서는 끊임없이 공부하고 인지하기.



choices를 어떻게 지우느냐

quizCodeBox.textContent = "";

⛔ 내가 사용했던 방식
while ($choiceAnswerBox.hasChildNodes()) {
        $choiceAnswerBox.removeChild($choiceAnswerBox.lastChild);
    }
    
✅ 위에 코드보단 아래 코드를 더 추천하는 느낌?
 $choiceAnswerBox.textContent = "";


display

hidden클래스를 활용하여 숨겼다가 필요한 경우 다시 보여주고 내부의 값만 바꿔주는 방식
자바스크립트 상에서 클래스 설정 지양, classList 이용하기



스트링만 사용시 템플릿 리터럴 사용하지 않을 것

어쩌구 저쩌구 ~ = `틀렸습니다` ❌

템플릿 리터럴은 내장된 표현식을 허용하는 문자열 리터럴로 새로운 문자열을 삽입할 수 있는 기능을 제공하는데 이를 문자열 인터폴레이션(String Interpolation)이라 한다.
문자열 인터폴레이션${ … }으로 표현식을 감싼다. 문자열 인터폴레이션 내의 표현식은 문자열로 강제 타입 변환된다.

👉 즉, 스트링과 문자열 인터폴레이션을 동시에 같이 쓸 때 쓰고 그 외에 스트링만 사용할 경우에는 따옴표를 사용할 것.



전역변수 사용은 최대한 지양

지역스코프에서만 사용하는 지역변수는 지역스코프 안에서만 생성하고,
변수를 최상단의 배치할 경우 한번에 연속으로 적기보다는, 역할에 따라 개행을 주는 것이 가독성이 좋음



논리연산자

👉 앞 전 블로그 내용 참고
예를 들어, 이번 코드에서도
if (어쩌구.code === null) => if (!어쩌구.code) 와 같이 간결하게 사용하라.

if (어쩌구.code) = 값이 존재하므로 true 값이 되고, null은 애초에 false 값이므로
! 논리 연산자를 잘 응용할 것

자바스크립트에서 불리언 결과가 false로 판명되는 것들은 다음과 같다.

  • undefined, null

  • NaN

  • 0 (숫자 리터럴) , -0

  • “” (빈 문자열)

  • false

출처: https://studymake.tistory.com/484 [스터디메이크]



⭐⭐ 이벤트 위임에 대해서

innerText, innerHTML, textContent의 차이

👉 앞 전 블로그 내용 참고

innerHTML 같은 경우에는 렌더링이 되기 때문에 프로그램의 속도나 뭐시기에 영향을 줄 수 있으므로 현업에서는 주로 사용하지 않음.
때문에 html 효과를 같이 주어야할 때와 같은 정말 필요한 상황이 아닐 때에는
textContent를 주로 사용하여야 하며, innerText도 지양하는 추세이다

좋은 웹페이지 즐겨찾기