[JavaScript] 자바스크립트 9강 이차원 배열 다루기_틱택토 게임
9-1. 틱택토 순서도 그리기(테이블 만들기)
택택토게임 : 삼목(3x3 표 위에서 진행하는 게임)
🔆 순서도 작성
1. 시작
2. 3x3 이차원 배열을 준비한다.
3. o의 턴으로 설정한다.
4. 3x3 테이블을 그린다.
5. 대기
-----------------------------------
1. 칸을 클릭한다.
2. 클릭한 칸이 비어있는가?
❌: 대기
⭕: 현재 턴을 칸에 적어넣는다.
승부가 났는가?
⭕: 승자를 표시한다. → 끝
❌: 무승부인가?
⭕: 무승부라고 표시한다. → 끝
❌: 턴을 넘긴다. → 대기
🔆 테이블 만들기
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>틱택톡</title>
<style>
table {
border-collapse: collapse;
}
td {
border:1px solid black;
width: 40px;
height: 40px;
text-align: center;
}
</style>
</head>
<body>
<script>
// 3X3 배열 생성
const data = [];
for(let i = 0; i < 3; i++){
data.push([]); // 배열 안에 배열 i개 넣기
}
// table 생성
const $table = document.createElement('table');
document.body.append($table);
// document.body: body 태그 선택
// append(document.createElement("table")): body에 테이블 추가
</script>
</body>
</html>
🔆 테이블 형태
<table>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
3x3 테이블 형태는 다음과 같은 자바스크립트 코드로 구현할 수 있다.
<script>
...
const $table = document.createElement('table');
for(let i = 0; i < 3; i++) {
const $tr = document.createElement('tr')
for(let i =0; i < 3; i++){
const $td = document.createElement('td');
$tr.appendChild($td);
}
$table.append($tr);
}
document.body.append($table)
</script>
실행 화면
9-2. 이차원 배열 다루기
🔆 테이블에 글자 표시
<script>
let turn = 'O';
const data = [];
for(let i = 0; i < 3; i++){
data.push([]); // 배열 안에 배열 i개 넣기
}
// document.body: body 태그 선택
// append(document.createElement("table")): body에 테이블 추가
const $table = document.createElement('table');
for(let i = 0; i < 3; i++) {
const $tr = document.createElement('tr')
for(let i =0; i < 3; i++){
const $td = document.createElement('td');
$td.addEventListener('click', (event) => {
// 칸에 글자가 존재하는지 확인
if (event.target.textContent) return; // 글자 존재시 함수 멈추기
event.target.textContent = turn; // 클릭하면 텍스트 생성
// 승부 확인
// 턴 넘기기
if( turn === 'O') {
turn = 'X';
} else if (turn === 'X') {
turn = 'O';
}
});
$tr.appendChild($td);
}
$table.append($tr);
}
document.body.append($table);
</script>
실행 화면
9-3. 표 다시 그리기(구조분해 할당)
🔆 구조분해 할당(distructuring)
🟡 객체의 구조분해 할당
const body = document.body;
위 코드를 구조분해 할당한 코드는 다음과 같다.
const { body } = document; //구조분해 할당
- document: 객체
- body : 객체의 속성
- 어떤 객체의 속성과, 그 속성을 변수에 담는 변수명이 동일 할 때, 사용한다. (위 예의 경우, body)
- 객체 이름과 변수명이 동일해야 한다.
const body = document.body;
const createElement = document.createElement;
//위 코드를 아래와 같이 줄일 수 있다.
const { body, createElement } = document;
🟡 배열의 구조분해 할당
const arr = [1, 2, 3, 4, 5];
const one = arr[0];
const two = arr[1];
const three = arr[2];
const four = arr[3];
const five = arr[4];
const [one, two, three, four, five] = arr;
const [one, three, five] = arr; // 2,4 사용하지 않고 싶을 때
⭐ 배열의 경우 자릿수가 항상 동일해야 한다.(이 예제의 경우 5개)
예제)
const obj = {a: 1, b: 2};
const { a, b } = obj; // 다음 두 줄을 이렇게 한 줄로 표현 가능
const a = obj.a;
const b = obj.b;
🟡 1분 퀴즈: 구조분해 할당으로 바꿔보기
a, c, e 속성을 구조분해 할당 문법으로 변수에 할당해보세요
const obj = {
a: 'hello',
b: {
c: 'hi',
d: { e: 'wow' },
},
};
중괄호{} 안이 object 객체라고 생각하기
const { a, b: { c, d: { e } } } = obj;
const a = obj.a;
const c = obj.b.c;
const e = obj.b.c.d.e
번외로 b를 구조분해 할당 문법을 한다.
const { c ,d: {e}} = b
🟡 구조분해 할당으로 코드 리팩토링
const { body } = document;
const $table = document.createElement('table');
const $result = document.createElement('div'); // 결과창
const rows = [];
let turn = 'O';
for(let i = 1; i <= 3; i++) {
const $tr = document.createElement('tr');
const cells = [];
for(let j = 1; j <= 3; j++){
const $td = document.createElement('td');
cells.push($td);
$td.addEventListener('click', (event) => {
// 칸에 글자가 존재하는지 확인
if (event.target.textContent) return; // 글자 존재시 함수 멈추기
event.target.textContent = turn; // 클릭하면 텍스트 생성
// 승부 확인
// 턴 넘기기
if( turn === 'O') {
turn = 'X';
} else if (turn === 'X') {
turn = 'O';
}
});
$tr.appendChild($td);
}
rows.push(cells);
$table.append($tr);
}
document.body.append($table);
document.body.append($result);
</script>
이렇게 코드를 짜면 위와 같이 몇 번째 줄, 몇 번째 칸에 정보를 넣을지 검색하거나 지정하기가 수월해진다.
🟡 1분 퀴즈: 5(줄)X4(칸)짜리 이차원 만들기. 배열의 요소는 모두 1임
const array = [];
for (let i = 0; i < 5; i++) {
const cells = [];
for (let j = 0; j < 4; j++ ){
cells.push(i)
}
array.push(cells)
}
console.log(array);
9-4. 차례 전환하기
<script>
const { body } = document;
const $table = document.createElement('table');
const $result = document.createElement('div'); // 결과창
const rows = [];
let turn = 'O';
const callback = (event) => {
if (event.target.textContent !== '') { // 칸이 이미 채워져 있는가?
console.log('빈칸이 아닙니다.');
return;
}// 글자가 존재하지 않을 때
console.log('빈칸입니다');
event.target.textContent = turn;
// 삼항 연산자로 바꾼 것임
turn = ( turn === 'X' ? 'O' : 'X' );
};
for (let i = 1; i <= 3; i++) {
const $tr = document.createElement('tr');
const cells = [];
for (let j = 1; j <= 3; j++) {
const $td = document.createElement('td');
cells.push($td);
$td.addEventListener('click', callback);
$tr.appendChild($td);
}
rows.push(cells);
$table.append($tr);
}
document.body.append($table);
document.body.append($result);
</script>
🟡 이전 코드와 다른 점
- callback 함수를 외부로 빼주었다
- 삼항연산자를 이용해 turn 값을 변경하도록 코드를 수정하였다.
9-5. 이벤트 버블링, 캡처링
if (event.target.textContent) return;
- 위 코드와 같이 return으로 종료하는 코드도 가능하지만, removeEventListnener를 이용해 이벤트를 제거해주는 방법도 존재한다.
- 게임 도중에는 return을, 게임이 완전히 종료했을 시에는 removetListener를 사용하는 것이 좋다.
- 이벤트 리스너 addEventListener로 이벤트 리스너를 붙여주었다면, 그 수만큼 removeEventListener로 이벤트 리스너를 제거해 주어야 한다.
🔆 이벤트 버블링
for (let i = 1; i <= 3; i++) {
const $tr = document.createElement('tr');
const cells = [];
for (let j = 1; j <= 3; j++) {
const $td = document.createElement('td');
cells.push($td);
$td.addEventListener('click', callback);
$tr.appendChild($td);
}
rows.push(cells);
$table.append($tr);
}
// table에 이벤트 리스너 추가
$table.addEventListener('click', callback);
document.body.append($table);
document.body.append($result);
위 코드는 이전 예제와 동일하게 동작한다.
html 특성에 따라, td에서 클릭 이벤트가 발생하면 td의 상위 이벤트가 전달되어 tr,table,body에서도 이를 감지할 수 있다. 따라서 위 코드의 경우에도 table에 달아놓은 이벤트 리스너가 정상적으로 동작한다. 이를 이벤트 버블링이라고 한다.
E또한 상위에 이벤트 리스너 하나만 달아도 실행이 되므로 추후 이벤트리스너 제거 시에도 removeEventListener를 한번만 수행해도 된다.
🟡 필요성
ex) td 태그 안에 span 태그가 존재하고 이 span 태그에 이벤트 리스너를 달았다고 가정했을 때, 이벤트 버블링이 존재하지 않는다면 span 태그를 클릭했을 때의 이벤트가 td에 전달되지 않는다.
🟡 tr이 아닌, table에 접근하고 싶을 때
event.target //td에 접근
event.currentTarget //table에 접근 가능
🟡 이벤트 버블링 방지 방법
const callback = (event) => {
event.stopPropagation(); // 이벤트 버블링 방지
...
};
🔆 이벤트 캡처링
$table.addEventListener("click", callback, true);
- 세번째 인수를 true로 했을 시 캡처링, 기본은 flase(버블링)
- 부모 클릭시 이벤트가 자식에게 전달됨
- 잘 사용하지 않는다.
- 활용) 팝업 외부(부모) 클릭시 클릭 이벤트가 팝업(자식)에 전달되어 팝업이 닫히도록 만들 수 있다.
🟡 1분 퀴즈:이벤트 버블링 예제
버튼을 클릭 할 때 'hello, event bubbling'을 alert 한 다음, 리스너를 button 태그에 달아서는 안됨
<header>
<div>
<button>클릭</button>
</div>
</header>
<script>
// button 위 사으이 요소 div, 그 상위 요소 header에 하면 된다.
document.querySelector('header').addEventListener('click', () => {
console.log('hello, event bubbling');
});
</script>
9-6. 승부 판단하기
<script>
...
let turn = "O";
// [
// [td, td, td],
// [td, td, td],
// [td, td, td],
// ]
const checkWinner = (target) => {
//target: td
let rowIndex;
let cellIndex;
//target과 row, cell을 하나씩 비교해보며 같은지 확인
rows.forEach((row, ri) => {
row.forEach((cell, ci) => {
if (cell === target) {
rowIndex = ri;
cellIndex = ci;
}
});
});
//세 칸이 모두 채워졌는지(승자가 존재하는지) 검사
let hasWinner = false;
//가로줄 검사
if (
rows[rowIndex][0].textContent === turn &&
rows[rowIndex][1].textContent === turn &&
rows[rowIndex][2].textContent === turn
) {
hasWinner = true;
}
//세로줄 검사
if (
rows[0][cellIndex].textContent === turn &&
rows[1][cellIndex].textContent === turn &&
rows[2][cellIndex].textContent === turn
) {
hasWinner = true;
}
//대각선 검사
if (
rows[0][0].textContent === turn &&
rows[1][1].textContent === turn &&
rows[2][2].textContent === turn
) {
hasWinner = true;
}
if (
rows[0][2].textContent === turn &&
rows[1][1].textContent === turn &&
rows[2][0].textContent === turn
) {
hasWinner = true;
}
return hasWinner;
};
const callback = (event) => {
//칸에 글자가 존재하는지 확인
if (event.target.textContent !== "") {
console.log("빈칸이 아닙니다");
return;
}
//글자가 존재하지 않을 때
console.log("빈칸입니다");
event.target.textContent = turn;
//승부확인 -> 승자가 있는지 확인
if (checkWinner(event.target)) {
$result.textContent = `${turn}님의 승리!`;
//승리 후 더 이상 클릭되지 않게 이벤트 리스너 제거
$table.removeEventListener("click", callback);
return;
}
//무승부 검사
let draw = true;
rows.forEach((row) => {
row.forEach((cell) => {
if (!cell.textContent) {
draw = false; //한칸이라도 비어있으면 무승부가 아님
}
});
});
if (draw) {
$result.textContent = "무승부";
return;
}
//턴 넘기기
turn = turn === "O" ? "X" : "O";
};
...
</script>
- 칸을 하나씩 검사하면서 가로줄, 세로줄, 대각선에 동일한 글자가 오면 승리를 출력하고 이벤트 리스너를 제거하여 게임을 종료한다.
- 칸이 모두 차도 승부가 나지 않으면 무승부를 출력하고 게임을 종료시킨다.
9-7. 부모자식 관계, 유사배열, every, some, flat
🔆 부모자식 관계
<script>
const checkWinner = (target) => {
//target: td
let rowIndex;
let cellIndex;
//target과 row, cell을 하나씩 비교해보며 같은지 확인
rows.forEach((row, ri) => {
row.forEach((cell, ci) => {
if (cell === target) {
rowIndex = ri;
cellIndex = ci;
}
});
});
...
};
...
</script>
▼
<script>
const checkWinner = (target) => {
//target: td
const rowIndex = target.parentNode.rowIndex; //td의 부모 태그의 rowIndex 가져오기
const cellIndex = target.cellIndex;
//반복문 돌며 찾을 필요 없으므로 forEach문은 삭제
...
};
...
</script>
🟡 target(td)의 cellIndex를 바로 알 수 있는 코드
const cellIndex = target.cellIndex;
🟡 rowIndex는?
const rowIndex = target.parentNode.rowIndex;
- parentNode : target의 부모태그를 가리킴
- td의 부모님 tr의 rowIndex
태그.children은 유사배열. (배열처럼 생겼지만 배열이 아님. forEach 못 씀)
🔆 유사배열
HTMLCollection(3) [tr, tr, tr]
- 위와 같은 배열앞에 이름(HTMLCollection)이 붙어있는 경우 배열이 아닌 유사배열이므로 배열의 함수인 forEach를 쓸 수 없다.
- Array.from()를 사용하면 유사배열을 배열로 바꿀 수 있다.
🔆 every
🟡 비효율적인 코드
<script>
//무승부 검사
let draw = true;
rows.forEach((row) => {
row.forEach((cell) => {
if (!cell.textContent) {
draw = false; //한칸이라도 비어있으면 무승부가 아님
}
});
});
</script>
[
['', td, td],
[td, td, td],
[td, td, td],
]
기존에 작성한 코드는 위와 같이 첫번째 칸이 비어있는 경우에도 모든 칸을 반복하여 순회하므로 비효율적이다.
비어있는 칸이 발견되자마자 코드를 종료하고 flase를 리턴할 수 있도록 코드를 작성해야 합니다.
이때 forEach문을 준간에 멈추게 할 수 있는 방법으로 every 메소드를 사용한다.
every 메소드를 사용해 조건이 모두 true일 경우(빈칸이 하나도 없는 경우) true,
하나라도 조건에 부합하지 않으면(빈칸이 하나라도 존재하면) false를 리턴하도록 코드를 작성한다.
🟡 every 메소드
- 1차원 배열에만 사용 가능
- 2차원 배열에서 사용하고 싶다면? 배열명.flat() 을 사용 👉 flat 메소드: 2차원 배열이 1차원 배열로 펴짐
🟡 flat 메소드 + every 메소드
🟡 flat 메소드 + some 메소드
- some 메소드: 조건이 하나라도 true라면 true를 리턴
👩 false가 되는 값 6가지 외우기
- 문자열- 빈문자열
- boolean - false
- 숫자 - 0
- null
- undefined
- not
🟡 효율적으로 수정한 코드
<script>
//무승부 검사
let draw = rows.flat().every((cell) => cell.textContent);
if (draw) {
$result.textContent = "무승부";
return;
}
</script>
👉 하나라도 false가 되면 false를 리턴하고 종료됨.(전체 코드를 돌지 않는다.)
🕵️♀️ flat() 메소드 관련 추가정보
- 3차원 배열에 flat 함수를 사용하면 2차원 배열이 된다. 여기에 한번 더 flat을 사용하면 1차원 배열이 된다.
- 1차원 배열은 flat을 해도 1차원 배열이다.
🟡 1분 퀴즈 : every, some 예제
한 칸이라도 null이 들어 있으면 true 반환, 아니면 flase -> some 함수 사용해야 됨
const array = [1, 'hello', null, undefined, flase];
// 답
array.some((value) => value === null);
console.log(array); // true
최종 코드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>틱택톡</title>
<style>
table {
border-collapse: collapse;
}
td {
border: 1px solid black;
width: 40px;
height: 40px;
text-align: center;
}
</style>
</head>
<body>
<script>
const { body } = document;
const $table = document.createElement('table');
const $result = document.createElement('div'); // 결과창
const rows = [];
let turn = 'O';
// [
// [td, td, td]
// [td, td, td]
// [td, td, td]
// ]
const checkWinner = (target) => {
//target: td
let rowIndex = target.parentNode.rowIndex;
// td 부모 태그의 rowIndex 가져오기
let cellIndex = target.cellIndex;
// 세 칸이 모두 채워졌는지(승자가 존재하는지) 검사
let hasWinner = false;
//가로줄 검사
if (
rows[rowIndex][0].textContent === turn &&
rows[rowIndex][1].textContent === turn &&
rows[rowIndex][2].textContent === turn
) {
hasWinner = true;
}
//세로줄 검사
if (
rows[0][cellIndex].textContent === turn &&
rows[1][cellIndex].textContent === turn &&
rows[2][cellIndex].textContent === turn
) {
hasWinner = true;
}
//대각선 검사
if (
rows[0][0].textContent === turn &&
rows[1][1].textContent === turn &&
rows[2][2].textContent === turn
) {
hasWinner = true;
}
if (
rows[0][2].textContent === turn &&
rows[1][1].textContent === turn &&
rows[2][0].textContent === turn
) {
hasWinner = true;
}
return hasWinner;
};
const callback = (event) => {
// 칸에 글자가 존재하는지 확인
if (event.target.textContent !== "") {
console.log("빈칸이 아닙니다");
return;
}
//글자가 존재하지 않을 때
console.log("빈칸입니다");
event.target.textContent = turn;
//승부확인 -> 승자가 있는지 확인
if (checkWinner(event.target)) {
$result.textContent = `${turn}님의 승리!`;
//승리 후 더 이상 클릭되지 않게 이벤트 리스너 제거
$table.removeEventListener("click", callback);
return;
}
//무승부 검사
let draw = rows.flat().every((cell) => cell.textContent);
if (draw) {
$result.textContent = "무승부";
return;
}
// 턴 넘기기
turn = (turn === 'X' ? 'O' : 'X');
};
for (let i = 1; i <= 3; i++) {
const $tr = document.createElement('tr');
const cells = [];
for (let j = 1; j <= 3; j++) {
const $td = document.createElement('td');
cells.push($td);
$tr.append($td);
}
rows.push(cells);
$table.append($tr);
}
$table.addEventListener('click', callback);
body.append($table);
body.append($result);
</script>
</body>
</html>
결과 화면
참고 영상