Javascript

[JavaScript] 자바스크립트 9강 이차원 배열 다루기_틱택토 게임

양치개발자 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>

🟡 이전 코드와 다른 점

  1. callback 함수를 외부로 빼주었다
  2. 삼항연산자를 이용해 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가 되는 값이 포함되어&nbsp;있다면 every 종료

🟡  flat 메소드 + every 메소드

첫번째 칸부터 비어있으므로 flase 리턴

🟡  flat 메소드 + some 메소드

  • some 메소드: 조건이 하나라도 true라면 true를 리턴

하나라도 칸이 비어있는 경우 true를 리턴하도록 작성한 코드

 

👩  false가 되는 값 6가지 외우기

  1. 문자열- 빈문자열
  2. boolean - false
  3. 숫자 - 0
  4. null
  5. undefined
  6. 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승리
무승부

 

 

참고 영상

https://tinyurl.com/w93bj843

 

반응형