-
[JavaScript] 자바스크립트 9강 이차원 배열 다루기_틱택토 게임Javascript 2023. 6. 7. 22:19반응형
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>
이렇게 코드를 짜면 위와 같이 몇 번째 줄, 몇 번째 칸에 정보를 넣을지 검색하거나 지정하기가 수월해진다.
콘솔에서 rows를 찍어본 결과 🟡 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차원 배열로 펴짐
하나라도 조건이 false가 되는 값이 포함되어 있다면 every 종료 🟡 flat 메소드 + every 메소드
첫번째 칸부터 비어있으므로 flase 리턴 🟡 flat 메소드 + some 메소드
- some 메소드: 조건이 하나라도 true라면 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>
결과 화면
O 승리 X승리 무승부 참고 영상
반응형'Javascript' 카테고리의 다른 글
[JavaScript] 자바스크립트 10장. 클래스_텍스트 RPG 게임 만들기 (0) 2023.06.09 [JavaScript] 자바스크립트 9강.셀프 체크 - 컴퓨터의 턴 만들기 (0) 2023.06.08 [JavaScript] 자바스크립트 8강 셀프 체크 - 속도 순으로 정렬하기 (0) 2023.06.06 [Javascript] 자바스크립트 8장 Date 사용하기_반응속도 테스트 (0) 2023.06.05 [Javascript] 자바스크립 셀프 체크 - 3판 2선승제로 만들기 (0) 2023.06.02