미니 프로젝트 네 번째 게임으로 기억력 테스트 게임을 만들었습니다.
PC의 블록 선택 순서를 기억한 후, 순서를 똑같이 맞추는 게임입니다.
HTML, CSS, JavaScript를 이용하여 만들었습니다.
let answerCount = 1; // 정답 개수
let answerArr = []; // 정답을 저장하는 배열
function createAnswer() {
for (let i = 0; i < answerCount; i++) {
answerArr.push(getRandom(9, 0));
}
}
function getRandom(max, min) {
return parseInt(Math.random() * (max - min)) + min;
}
Math.random()
메소드를 이용해 0부터 9 미만의 값을 정답 개수 만큼 무작위로 뽑았습니다. 뽑은 값은 정답을 저장하는 배열에 넣었습니다.
배경색이 바뀌며 PC의 선택 값을 보여주는 기능을 만들었습니다.
HTML
<div id="container" class="no-drag">
<div class="board">
...
<main class="game">
<div class="item-wrapper">
<div class="item" data-id="0"></div>
<div class="item" data-id="1"></div>
<div class="item" data-id="2"></div>
<div class="item" data-id="3"></div>
<div class="item" data-id="4"></div>
<div class="item" data-id="5"></div>
<div class="item" data-id="6"></div>
<div class="item" data-id="7"></div>
<div class="item" data-id="8"></div>
</div>
</main>
...
</div>
</div>
.item
은 각각의 블록을 의미합니다.
CSS
.bgChange {
animation: bgChangeEffect 400ms 2 alternate;
}
@keyframes bgChangeEffect {
from {
background-color: #FFF;
}
to {
background-color: #DEA5A4;
}
}
CSS를 통해 배경색이 흰색에서 빨간색으로 변하는 애니메이션을 만들었습니다.
그리고 그 애니메이션을 실행하는 .bgChange
클래스를 만들었습니다.
JavaScript
const items = document.getElementsByClassName("item");
function selectAnswerOnPC() {
let cnt = 0;
let selectAnswerPromise = new Promise((resolve, reject) => {
let selectAnswerTimer = setInterval(() => {
// 배경색이 변하는 애니메이션 재시작을 위해 이미 select 클래스가 부착되어 있다면 제거
if (items[answerArr[cnt]].classList.contains("bgChange")) {
items[answerArr[cnt]].classList.remove("bgChange");
void items[answerArr[cnt]].offsetWidth;
}
items[answerArr[cnt++]].classList.add("bgChange");
if (cnt === answerCount) {
clearInterval(selectAnswerTimer);
resolve();
}
}, 800);
});
selectAnswerPromise.then(() => {
// selectAnswerPromise 성공인 경우 실행할 코드
setTimeout(waitGameStart, 800);
});
}
자바스크립트로 setInterval()
을 이용해 0.8초마다 PC의 선택 값을 하나씩 보여주도록 했습니다.
이때 classList.add()
를 통해 .bgChange
클래스를 추가하여 배경색이 바뀌는 애니메이션이 동작하도록 했습니다.
선택 값에 .bgChange
클래스가 이미 부착되어 있다면 애니메이션이 동작하지 않으므로 classList.remove()
를 통해 .bgChange
클래스를 제거한 후 다시 .bgChange
클래스를 추가하였습니다.
items[answerArr[cnt]].classList.remove("bgChange");
items[answerArr[cnt]].classList.add("bgChange");
이런 식으로 클래스 remove 후 add를 하면 애니메이션이 재시작될 줄 알았는데, 실제로는 애니메이션이 재시작되지 않았습니다.
items[answerArr[cnt]].classList.remove("bgChange");
void items[answerArr[cnt]].offsetWidth;
items[answerArr[cnt]].classList.add("bgChange");
검색 결과 이렇게 remove, add 중간에 void HTMLElement.offsetWidth
코드를 추가하면 애니메이션 재시작이 가능하다는 걸 알게 되었고, 코드 추가 후 실제로도 애니메이션 재시작이 잘 됐습니다.
대체 왜 되는 걸까..🤔? 이유를 알기 위해 검색을 좀 더 해봤습니다.
- void : void 연산자는 주어진 표현 식을 평가하고 undefined를 반환합니다.
- offsetWidth : HTMLElement.offsetWidth은 레이아웃의 너비를 반환합니다.
- 참고한 글 : [TIL #35] JavaScript로 CSS 애니메이션 재시작하기
결국 void HTMLElement.offsetWidth
는 HTML 엘리먼트의 너비를 구한 후 최종적으로는 undefined
를 반환하는 코드라는 것을 알 수 있었습니다.
콘솔에 찍어보니 실제로 void items[answerArr[cnt]].offsetWidth
는 undefined
값을 반환하고 있었습니다.
즉, 해당 코드는 브라우저에 의미 없는 계산을 하게 시키는 코드였습니다.
클래스를 붙이고 지울 때마다 변경 사항을 렌더링 하는 것은 리소스가 많이 드는 일입니다.
그래서 일반적인 브라우저들은 이렇게 리소스가 많이 드는 변경 사항들은 바로 적용 하지 않고 기록만 해놓은 후, 작업이 끝난 뒤에 일괄 처리를 진행한다고 합니다.
제가 처음에 짰던 코드를 실행하면
items[answerArr[cnt]].classList.remove("bgChange");
items[answerArr[cnt]].classList.add("bgChange");
브라우저는 아래와 같이 동작하게 됩니다.
bgChange
클래스를 제거한다.- 브라우저는 변경 사항을 기록하지만, 화면을 렌더링하지 않는다.
bgChange
클래스를 추가한다.- 함수를 다 읽은 브라우저는 이제 기록해놓은 일들을 처리하려고 하는데.. 잉? 처음이랑 달라진 게 없네?🤔
- 화면은 그대로 유지된다.
이런 이유로 애니메이션이 재시작되지 않은 것이었습니다.
중간에 offsetWidth
를 구하는 코드를 추가한다면 브라우저는 offsetWidth
를 알아내기 위해 변경 사항을 일괄 처리하는 계획을 포기하고 바로 당장 페이지 렌더링을 수행하게 됩니다.
따라서 아래와 같이 클래스 제거 후 offsetWidth
를 요청하는 코드를 넣으면
items[answerArr[cnt]].classList.remove("bgChange");
void items[answerArr[cnt]].offsetWidth;
items[answerArr[cnt]].classList.add("bgChange");
브라우저는 아래와 같이 동작하게 됩니다.
bgChange
클래스를 제거한다.- 브라우저는 변경 사항을 기록하지만, 화면을 렌더링하지 않는다.
- 브라우저는
offsetWidth
를 구한 후 화면을 렌더링한다.bgChange
클래스를 추가한다.bgChange
클래스 제거가 화면에 반영된 상태이므로(3번 과정) 애니메이션이 동작한다.
그래서 저는 classList.contains()
메소드를 이용해 bgChange
클래스가 이미 붙어있는 경우에만 해당 연산을 수행하도록 하는 조건문을 추가하여 구현했습니다.
const itemWrapper = document.getElementsByClassName("item-wrapper")[0];
itemWrapper.addEventListener("click", function(e) {
...
let targetId = parseInt(e.target.dataset.id);
checkCorrectAnswer(targetId);
});
클릭 이벤트가 발생할 때마다 사용자가 클릭한 블록의 id 값을 얻어왔습니다.
이렇게 얻어온 id 값을 PC의 선택 값과 비교하는 함수로 전달했습니다.
function checkCorrectAnswer(targetId) {
// 사용자가 선택한 블록의 id와 정답이 일치하면 맞은 것으로 판단
if (targetId === answerArr[playerSelectionCount++]) {
// 사용자의 선택 횟수가 정답 개수(PC의 선택값 개수)와 같아지면 전부 맞은 것으로 판단
if (playerSelectionCount === answerCount) {
clearStage();
}
} else {
stopGame();
}
}
사용자가 클릭한 블록의 id와 정답이 일치하면 맞은 것으로 판단했습니다.
그리고 사용자의 선택 횟수가 정답 개수와 같아지면 전부 맞은 것으로 판단하여 다음 스테이지로 넘어가도록 했습니다.
게임이 종료되도록 했습니다.