[우테코]Level1 로또 게임 2단계
미션2 🎱로또 게임 2단계
🔥2단계 구현 사항
1. 정적인 부분은 html에 미리 구현
1. main__title-wrapper
title은 화면에 진입했을 때 가장 먼저 보이는 부분이고 동적으로 바뀌지 않기 때문에 html에 코드를 짰다.
index.html
<div class="main__title-wrapper">
<h2 class="main__title">🎱 내 번호 당첨 확인 🎱</h2>
</div>
2. purchase-form
구입 금액 입력 부분도 첫 화면에 바로 보이는 부분이기 때문에 html에 모두 구현하였다.
index.html
<form class="purchase-form">
<label class="purchase-text">구입할 금액을 입력해주세요.</label>
<div class="purchase-input-wrapper">
<input
class="primary-input purchase-input"
placeholder="금액"
type="number"
min="1000"
max="100000"
/>
<button class="primary-button purchase-button" type="submit">구입</button>
</div>
</form>
2. 동적인 부분은 js로 구현
1. lottos-container
구입 금액에 따라 구입 개수
와 나의 로또 리스트
를 보여주는 부분이다.
.lottos-text
에는 구입 개수
를 동적으로 보여주고,
.lottos-list
에는 나의 로또 리스트
를 동적으로 보여주어야 한다.
html에는 아래와 같은 코드만 있지만 그림에서는 ul
태그 안에 li
태그들이 존재한다. 어디서 추가된걸까?
바로 JS에서 동적으로 넣어준 것이다.
index.html
<div class="lottos-container" hidden>
<label class="lottos-text">
<!-- 구입 개수 js로 구현 -->
</label>
<ul class="lottos-list">
<!-- 나의 로또 리스트 js로 구현 -->
</ul>
</div>
src/view/View.js
const $purchaseButton = elementHandler.$(".purchase-button");
const $lottosContainer = elementHandler.$(".lottos-container");
const $lottosText = elementHandler.$(".lottos-text");
const $lottosList = elementHandler.$(".lottos-list");
const View = {
renderPurchasedLottos(lottos) {
$lottosContainer.hidden = false;
$purchaseButton.disabled = true;
elementHandler.addClassList($purchaseButton, "disabled");
$lottosText.textContent = `총 ${lottos.length}개를 구매하였습니다.`;
$lottosList.innerHTML = `${lottos
.map(
(lotto) =>
`<li class="lottos-list-item"><span>🎟️</span> ${lotto.join(
", "
)}</li>`
)
.join("")}`;
},
};
2. winning-lotto-form: 정적+동적
구입 금액이 유효할 때 뜨는 부분이지만 입력 폼 자체는 정적이기 때문에 html에 미리 구현을 해두었고 hidden 속성만 달아놓았다.
index.html
<form class="winning-lotto-form" hidden>
<div class="winning-lotto-text">
지난 주 당첨번호 6개와 보너스번호 1개를 입력해주세요.
</div>
<div class="winning-lotto-container">
<div class="winning-lotto-wrapper">
<label>당첨 번호</label>
<div class="winning-lotto-input-container">
<!-- 당첨 번호 input js로 구현 -->
</div>
</div>
<div class="winning-lotto-wrapper">
<label>보너스 번호</label>
<div class="bonus-lotto-input-container">
<!-- 보너스 번호 input js로 구현 -->
</div>
</div>
</div>
<button
class="primary-button winning-lotto-submit-button"
type="submit"
value="결과 확인하기"
>
결과 확인하기
</button>
</form>
src/view/View.js
안내 문구나 label은 동적인 부분이지만 그 안의 input-container
는 입력칸이 반복적으로 렌더링 되는 부분이라 같은 코드를 html에 적는 것보다 js에서 반복하는 것이 효율적이라 생각하여 js에서 구현하였다.
repeat
을 사용해서 input
태그 6개를 한 번에 넣었다.
const $winningLottoForm = elementHandler.$(".winning-lotto-form");
const $winningLottoInputContainer = elementHandler.$(
".winning-lotto-input-container"
);
const $bonusLottoInputContainer = elementHandler.$(
".bonus-lotto-input-container"
);
const View = {
renderWinningNumbersInput() {
$winningLottoForm.hidden = false;
$winningLottoInputContainer.innerHTML =
`<input class="primary-input lotto-input winning" type="number" min="1" max="45" required />`.repeat(
SETTING.LOTTO_LENGTH
);
$bonusLottoInputContainer.innerHTML = `<input class="primary-input lotto-input bonus" type="number" min="1" max="45" required />`;
},
};
3. winning-result-table: 정적+동적
이 부분에서도 정적인 부분과 동적이 부분이 둘 다 있다.
<table class="winning-result-table">
<thead>
<tr>
<th class="winning-result-column head">일치 개수</th>
<th class="winning-result-column head">당첨금</th>
<th class="winning-result-column head">당첨 개수</th>
</tr>
</thead>
<tbody class="winning-result-container">
<!-- 당첨 결과 js로 구현 -->
</tbody>
</table>
표의 제목인 thead
태그 부분은 정적인 부분이기 때문에 html에 구현되어있다.
하지만 당첨 결과 부분은 동적인 부분이기 떄문에 tbody
안에 내용이 없다. js에서 동적으로 결과 표를 렌더링했다.
const $modal = elementHandler.$("#modal");
const $winningResultContainer = elementHandler.$(".winning-result-container");
const $profitRateText = elementHandler.$(".profit-rate-text");
const View = {
renderWinningResults(winningResults, profitRate) {
$modal.showModal(); // showModal: <dialog> 내장 함수
const matchedCounts = [3, 4, 5, "B5", 6];
$winningResultContainer.innerHTML = matchedCounts
.map((matchedKey) => {
const { TITLE_UI, REWARD } = RANKING[matchedKey];
return `
<tr>
<td class="winning-result-column">${TITLE_UI}</td>
<td class="winning-result-column">${REWARD.toLocaleString()}원</td>
<td class="winning-result-column">${winningResults[matchedKey]}개</td>
</tr>`;
})
.join("");
$profitRateText.textContent = `당신의 총 수익률은 ${profitRate}%입니다.`;
},
};
🔥2단계 피드백
1. 시맨틱 태그 사용하기
이전 코드
<header></header>
<div id="modal"></div>
<div id="main"></div>
<footer></footer>
피드백
modal은 <dialog>
, main 영역은 <main>
태그를 쓰면 더 좋을 것 같다.
수정한 코드
<header></header>
<dialog id="modal"></dialog>
<main id="main"></main>
<footer></footer>
dialog 태그 알아보기
1. 주요 특징
showModal()
로 열면 모달 외부 요소와의 상호작용이 차단된다.- ESC 키로 닫기 기능이 기본 제공된다.
- 접근성(WAI-ARIA) 지원이 기본으로 제공된다.(role=”dialog”, aria-modal=”true”, 포커스 트랩 등)
- 모바일 환경에서도 잘 작동한다.
form method="dialog"
를 사용하면 제출 시 자동으로 모달을 닫을 수 있다.
2. 주요 메서드
// 모달 열기
dialog.showModal(); // 모달 형태로 열기 (배경 딤처리, 외부 클릭 차단)
dialog.show(); // 일반 형태로 열기 (모달이 아닌 일반 요소처럼 표시)
// 모달 닫기
dialog.close(); // 모달 닫기
3. 주요 속성
// 모달이 열려있는지 확인
dialog.open; // true/false 반환
// 반환값 설정
dialog.returnValue; // form method="dialog"가 제출될 때의 반환값
4. 주요 이벤트
// 이벤트 리스너 예시
dialog.addEventListener("close", () => {
// 모달이 닫힐 때 실행
});
dialog.addEventListener("cancel", () => {
// ESC 키로 모달이 닫힐 때 실행
});
5. CSS 가상 클래스
/* 모달이 열려있을 때의 스타일 */
dialog::backdrop {
background: rgba(0, 0, 0, 0.5); /* 배경 딤처리 */
}
/* 모달이 열려있지 않을 때 */
dialog:not([open]) {
display: none;
}
6. 사용 예시
<dialog id="modal">
<form class="primary-container modal__content" method="dialog">
<button class="close-button">❌</button>
<h2 class="modal__title">🏆 당첨 통계 🏆</h2>
<!-- 당첨 결과 생략 -->
<button class="primary-button restart-button">다시 시작하기</button>
</form>
</dialog>
const View = {
renderWinningResults(winningResults, profitRate) {
$modal.showModal();
this.renderCloseModal();
this.renderRestartGame();
},
renderCloseModal() {
eventHandler.onClick($modal, () => $modal.close());
eventHandler.onClick($modalContent, (event) => {
event.stopPropagation();
});
},
renderRestartGame() {
eventHandler.onClick($restartButton, () => {
$modal.close();
});
},
};
2. innerHTML vs insertAdjacentHTML vs createElement
insertAdjacentHTML
1. 성능상의 이점: innerHTML은 전체 DOM을 다시 파싱하고 렌더링하지만, insertAdjacentHTML은 지정된 위치에만 새로운 요소를 삽입하므로 더 효율적이다.(채팅 등)
2. 기존 이벤트 리스너 보존: innerHTML은 기존 요소를 모두 제거하고 새로 만들기 때문에 이벤트 리스너가 사라진다. insertAdjacentHTML은 기존 요소와 이벤트 리스너를 유지한다.
3. XSS 공격에 취약: innerHTML과 마찬가지로 XSS 공격에 취약하다.
4. 누적이 아니라면 초기화 해야함: 초기화하지 않으면 새로운 데이터가 기존 데이터에 계속 추가된다.
beforebegin
<element>
afterbegin
내용
beforeend
</element>
afterend
const element = document.querySelector("#example");
// 1. 'beforebegin': 요소 바로 앞에
element.insertAdjacentHTML("beforebegin", "<div>이전에 추가</div>");
// 2. 'afterbegin': 요소의 첫 자식으로
element.insertAdjacentHTML("afterbegin", "<div>첫 자식으로 추가</div>");
// 3. 'beforeend': 요소의 마지막 자식으로
element.insertAdjacentHTML("beforeend", "<div>마지막 자식으로 추가</div>");
// 4. 'afterend': 요소 바로 뒤에
element.insertAdjacentHTML("afterend", "<div>다음에 추가</div>");
이전 코드
$lottosList.innerHTML = `${lottos
.map(
(lotto) =>
`<li class="lottos-list-item"><span>🎟️</span> ${lotto.join(", ")}</li>`
)
.join("")}`;
개선된 코드
$lottosList.textContent = ""; // 기존 내용 초기화
lottos.forEach((lotto) => {
$lottosList.insertAdjacentHTML(
"beforeend",
`<li class="lottos-list-item"><span>🎟️</span> ${lotto.join(", ")}</li>`
);
});
createElement
1. 장점
- 더 안전한 XSS 방어
- 더 나은 타입 안정성
- 더 명확한 코드 구조
- 복잡한 DOM 조작에서 더 나은 성능
2. 단점
- 더 많은 코드 작성 필요
- 간단한 HTML 구조에서는 덜 직관적
- 작은 규모의 단순 변경에서는 innerHTML이 더 빠를 수 있음
개선된 코드
const fragment = document.createDocumentFragment();
lottos.forEach((lotto) => {
const li = document.createElement("li");
li.className = "lottos-list-item";
const span = document.createElement("span");
span.textContent = "🎟️";
li.appendChild(span);
li.appendChild(document.createTextNode(` ${lotto.join(", ")}`));
fragment.appendChild(li);
});
$lottosList.textContent = ""; // 기존 내용 초기화
$lottosList.appendChild(fragment);
3. 구입 버튼 두 번 클릭 시 에러 발생
문제점
금액을 입력한 후 구입 버튼을 두 번 누르면 어떤 숫자를 입력해도 에러가 발생한다.
이유
src/controller/LottoGameController2.js
이벤트 리스너가 중복 등록되기 때문이다.
첫 번째 리스너는 입력값이 DOM에 반영되기 전에 실행되어 빈 값(‘‘)을 읽게 되고,
그 다음 두 번째 리스너가 실제 입력된 값을 읽어서 처리한다.
#inputWinningLotto() {
const $winningInputs = elementHandler.$$('.lotto-input.winning');
const $bonusInput = elementHandler.$('.lotto-input.bonus');
$winningInputs[0].focus();
// 이 이벤트 리스너가 구매 버튼을 누를 때마다 새로 등록됨
$winningLottoForm.addEventListener('submit', (event) => {
event.preventDefault();
const winningNumberList = [...$winningInputs].map((winningNumber) => Number(winningNumber.value));
const bonusNumber = Number($bonusInput.value);
// ...
});
}
수정한 코드
src/view/View.js
구입 버튼을 눌러 로또 리스트
가 화면에 렌더링 되면 $purchaseButton
에 disabled
속성을 true로 바꾸어서 중복 실행을 방지한다.
renderPurchasedLottos(lottos) {
$lottosContainer.hidden = false;
$purchaseButton.disabled = true;
elementHandler.addClassList($purchaseButton, 'disabled');
$lottosText.textContent = `총 ${lottos.length}개를 구매하였습니다.`;
$lottosList.innerHTML = `${lottos
.map((lotto) => `<li class="lottos-list-item"><span>🎟️</span> ${lotto.join(', ')}</li>`)
.join('')}`;
},
4. 유효성 검사 통과 못 했을 때 모달창 뜸
문제점 & 이유
src/controller/LottoGameController2.js
#checkBonusNumber
가 try 됐을 때 this.#showResult();
를 실행해서 당첨 번호의 유효성 검사가 통과하지 못 해도 보너스 번호의 유효성 검사가 통과한다면 모달창이 뜨는 오류가 발생했다.
#inputWinningLotto() {
// 생략
$winningLottoForm.addEventListener('submit', (event) => {
event.preventDefault();
const winningNumberList = [...$winningInputs].map((winningNumber) => Number(winningNumber.value));
const bonusNumber = Number($bonusInput.value);
this.#checkWinningNumbers(winningNumberList);
this.#checkBonusNumber(bonusNumber, winningNumberList);
});
}
#checkWinningNumbers(winningNumberList) {
const $winningInputs = elementHandler.$$('.lotto-input.winning');
try {
Validator.validateWinningNumberList(winningNumberList);
this.#winningNumbers = winningNumberList;
} catch (error) {
alert(error.message);
$winningInputs[0].focus();
}
}
#checkBonusNumber(bonusNumber, winningNumberList) {
try {
Validator.validateBonusNumber(bonusNumber, winningNumberList);
this.#bonusNumber = bonusNumber;
this.#showResult(); // 보너스 번호 유효성 검사만 통과해도 모달이 뜸
} catch (error) {
alert(error.message);
}
}
수정한 코드
src/controller/LottoGameController2.js
this.#showResult();
실행을 #inputWinningLotto()
메소드의 역할로 옮긴 후
이 메소드 안에서 두 검증을 모두 통과했는지 확인하고 통과했다면 모달창을 띄우는 것으로 변경했다.
#inputWinningLotto() {
const $winningInputs = elementHandler.$$('.lotto-input.winning');
const $bonusInput = elementHandler.$('.lotto-input.bonus');
$winningInputs[0].focus();
$winningLottoForm.addEventListener('submit', (event) => {
event.preventDefault();
const winningNumberList = [...$winningInputs].map((winningNumber) => Number(winningNumber.value));
const bonusNumber = Number($bonusInput.value);
// 두 검증을 모두 통과해야만 결과를 보여줌
if (this.#validateWinningNumbers(winningNumberList) &&
this.#validateBonusNumber(bonusNumber, winningNumberList)) {
this.#winningNumbers = winningNumberList;
this.#bonusNumber = bonusNumber;
this.#showResult();
}
});
}
// 검증 메서드들을 boolean을 반환하도록 수정
#validateWinningNumbers(winningNumberList) {
const $winningInputs = elementHandler.$$('.lotto-input.winning');
try {
Validator.validateWinningNumberList(winningNumberList);
return true;
} catch (error) {
alert(error.message);
$winningInputs[0].focus();
return false;
}
}
#validateBonusNumber(bonusNumber, winningNumberList) {
try {
Validator.validateBonusNumber(bonusNumber, winningNumberList);
return true;
} catch (error) {
alert(error.message);
return false;
}
}
🔥개념과 함께 채우기
DOM 변경을 비싼 작업이라고 하는 이유는 무엇일까?
DOM을 추가/삭제/수정하면 브라우저에서는 어떤 일이 일어날까?
이벤트 핸들러 함수를 만들 때 bind(this) vs 화살표 함수의 차이
this - 함수가 실행될 때 this가 어떤 걸로 결정되는 지 예상할 수 있는지?
일반 함수와 화살표 함수는 무슨 차이가 있을까?
- this binding, call/apply/bind
댓글남기기