레이싱 게임을 만들면서 학습한 것들을 정리해본다.
스터디를 하면서는 같은 기능을 다르게 구현한 남의 코드를 읽을 때, 피드백을 받고 리팩토링할 때가 배우는 게 정말 많다.
// Before
namesInput.addEventListener("keyup", ({ target }) => {
carNames = target.value;
// ...
});
function onGameTimesSubmit() {
// ...
gameTimesSubmitBtn.addEventListener("click", () => {
times = gameTimesInput.value;
//...
});
}
SUBMIT_ACTION
이라는 하나의 객체를 만들고, input
들의 상위 요소인 form
에 이벤트를 위임해 이벤트리스너를 등록해 불필요한 이벤트리스너를 호출하는 일을 줄였다.input
값을 submit
하는 버튼을 클릭하면 버튼의 data-action
값에 따라 다른 submit
함수가 실행되도록 리팩토링했다.// After
const SUBMIT_ACTION = {
submitNames: onCarNamesSubmit,
submitTimes: onGameTimesSubmit,
};
const $form = $("form", $el);
$form.addEventListener("click", e => {
e.preventDefault();
let action = e.target.dataset.action;
if (action) {
SUBMIT_ACTION[action]();
}
});
const namesArr = ["a", " b", " c ", " "]
namesArr
.map(name => name.replace(/(^\s*)|(\s*$)/gi, ""))
// ["a", "b", "c", ""]
.filter(name => !!name);
// ["a", "b", "c"]
namesArr
.filter(Boolean);
.map(name => name.trim())
// Before
function isNameUnderFiveWords(name) {
const isUnderFiveWords = /^.{0,5}$/;
if (isUnderFiveWords.test(name)) {
// ....
}
}
// After
function isNameUnderFiveWords(name) {
if (name.length < 5) {
return name;
}
// ...
}
// Before
const maxDistance = carsDistance.reduce((previous, current) => {
return previous > current ? previous : current;
});
let index = carsDistance.indexOf(maxDistance);
while (index !== -1) {
winner.push(carNames[index]);
index = carsDistance.indexOf(maxDistance, index + 1);
}
// After
function pickWinner() {
const maxDistance = Math.max(...carsDistance);
const winnerNames = carsDistance.reduce((names, cur, idx) => {
if (cur !== maxDistance) return names;
names.push(carNames[idx]);
return names;
}, []);
winner.push(...winnerNames);
}
filter(Boolean)
은 어떻게 동작하는 걸까?
1. filter는 배열의 각 요소에 대해 Boolean을 한 번 호출한다.
2. Boolean 생성자 함수는 배열에서 falsy한 값을 지우고 truthy한 값만을 리턴해 새로운 배열을 생성한다.
3. 즉, Boolean
을 iterator
로 사용하여 JS에서 falsy한 값(false, 0, -0, 0n, "", null, undefined, NaN)을 제거할 수 있다.
filter() 구문
arr.filter(callback(element[, index[, array]])[, thisArg])
1. filter()는 배열의 각 요소에 대해 제공된 콜백 함수를 한 번 호출한다.
2. 콜백이 true로 강제 변환되는 값을 반환하는 모든 값의 새 배열을 생성한다.
3. 콜백은 값이 할당된 배열의 인덱스에 대해서만 호출된다. 삭제되었거나 값이 할당된 적이 없는 인덱스에 대해서는 호출되지 않는다. 즉, 콜백 테스트를 통과하지 못한(false로 형변환되는) 배열 요소는 단순히 건너뛰고 새 배열에 포함되지 않는다.
// Before
const nameLengthCheck = nameBlankCheck.map(x => isNameUnderFiveWords(x));
return nameLengthCheck;
// After
return nameBlankCheck.map(x => isNameUnderFiveWords(x));
// Before
function onGameReset(timerId) {
const gameTimes = $("#times");
const gameTimesInput = $("#times input");
const gameTimesSubmitBtn = gameTimes.querySelector("button");
const gameResetBtn = gameWinner.querySelector("button");
gameResetBtn.addEventListener("click", () => {
nameCards.innerHTML = "";
form.lastElementChild.innerHTML = "";
gameWinner.innerHTML = "";
namesInput.disabled = false;
nameSubmitBtn.disabled = false;
gameTimesInput.disabled = false;
gameTimesSubmitBtn.disabled = false;
namesInput.value = "";
gameTimesInput.value = "";
carNames = [];
times = "";
carsDistance = [];
winner = [];
clearTimeout(timerId);
});
}
function onGameReset(timerId) {
const gameResetBtn = $("button", gameWinner);
gameResetBtn.addEventListener("click", () => {
preventInput("name", false);
resetNameState();
preventInput("gameTimes", false);
resetGameTimeState();
resetState();
form.lastElementChild.innerHTML = "";
clearTimeout(timerId);
});
}
function resetState() {
carNames = [];
times = "";
carsDistance = [];
winner = [];
}
function resetNameState() {
nameCards.innerHTML = "";
namesInput.value = "";
}
function resetGameTimeState() {
const gameTimesInput = $("#times input");
gameWinner.innerHTML = "";
gameTimesInput.value = "";
}
이제까지 JS를 불러올 때index.html
문서의 head
에서 defer
속성을 사용해 스크립트를 불러왔다. 그런데 모듈을 하는 경우 defer 속성을 따로 적어주지 않아도 된다고 한다.
<script type="module" src="src/js/main.js" defer></script>
- 모듈 스크립트를 불러올 때 defer 속성을 사용할 필요가 없습니다. 모듈은 자동으로 defer됩니다.
- 로컬 테스트에서의 주의 사항 — HTML파일을 로컬(예를들어 file:// URL)에서 로드하려고 하면, 자바스크립트 모듈 보안 요구 사항으로 인해 CORS오류가 발생합니다. 서버를 통해 테스트 해야 합니다.
mdn을 읽다보니 import, export, as 키워드만 사용해왔는데 Dynamic module loading이라는 것도 가능하다!
let squareBtn = document.querySelector('.square');
squareBtn.addEventListener('click', () => {
import('/js-examples/modules/dynamic-module-imports/modules/square.js').then((Module) => {
let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();
})
});