ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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>

    🟡 이전 코드와 다른 점

    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

     

    반응형

    댓글

Designed by Tistory.