본문 바로가기
💻CODING/react. vue

[react] react로 틱택토 게임 만들기(ft. useReducer)

by 코딩하는 갓디노 2021. 4. 6.

틱택토 게임

 

리액트 hooks로 구현한
틱택토 게임 입니다.

 

예제는 인프런의 제로초, "조현영"님의 강의를 들으면서 공부한 내용입니다.

 

순수 자바스크립트로 만든 로또 게임은 아래 포스트로 이동해주세요.

https://goddino.tistory.com/102

 

[js] 자바스크립트로 틱택토 구현하기

자바스크립트를 이용하여, 틱택토(Tic Tac Toe) 게임을 구현하는 예제입니다. 예제는 인프런의 제로초, "조현영"님의 강의를 들으면서 공부한 내용입니다. 순서도 css 코드 순수 자바스크립트 코드

goddino.tistory.com

https://goddino.tistory.com/113

 

[js] 자바스크립트로 틱택토(심화버전) 구현하기

이전 포스팅에서 구현했던 틱택토 게임에서 업그레이드된 심화 과정입니다. 예제는 인프런의 제로초, "조현영"님의 강의를 들으면서 공부한 내용입니다. 틱택토 구현 BEFORE 버전은 아

goddino.tistory.com

 

컴포넌트 구조

Tictactoe.jsx >Table.jsx > Tr.jsx > Td.jsx

· state 데이터는 상위 Tictactoe.jsx에서 관리하고, 클릭이벤트는 가장 하위 컴포넌트, Td.jsx에서 합니다. 
· state 데이터는 props를 통해 상위에서 하위로 전달됩니다. 

 

client.jsx

import React from 'react';
import ReactDom from 'react-dom';
import Tictactoe from './Tictactoe';

ReactDom.render(<Tictactoe />, document.querySelector('#root'));

 

index.html

<!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 { width: 60px; height: 60px; border: 1px solid #666; font-size: 30px; text-align: center;}
    </style>
</head>

<body>
    <div id="root"></div>
    <script src="./dist/app.js"></script>
</body>

</html>

 

Tictactoe.jsx - hooks 방식

import React, { useState, useEffect, useReducer, useCallback } from 'react';
import Table from './Table';

const initialState = {
    turn: 'O',
    winner: '',
    tableData: [['','',''], ['','',''], ['','','']], //2차원 빈 배열
    recentCell: [-1, -1], //최근 클릭한 셀, 초기화로 없는 셀을 넣음
}

export const SET_WINNER = 'SET_WINNER'; 
export const CLICK_CELL = 'CLICK_CELL'; //export로 모듈화, td 컴포넌트에서 재사용
export const CHANGE_TURN = 'CHANGE_TURN';
export const RESET_GAME = 'RESET_GAME';

const reducer = (state, action) => { 
    switch(action.type) {
        case SET_WINNER: {
        //state.winner = action.winner는 안됨, 새로운 action 객체를 만들어서 바뀐 값만 바꿔야함
         return {
             //새로운 state를 만들고 바뀌는 부분만 바꿔줌   
             ...state, //spread 문법 = 새롭게 얕은 복사(참조) 
             winner: action.winner, //바뀌는 부분
         }
        }
         case CLICK_CELL: {//찾은 위치에 턴 표시
         const tableData = [...state.tableData]; //기존 tableData를 새롭게 얕은 복사
         //td의 dispatch 안 action.row, 객체가 있으면 얕은 복사함
         tableData[action.row] = [...tableData[action.row]]; 
         tableData[action.row][action.cell] = state.turn;
         return {
             ...state, 
             //바뀐 값, 불변성 때문에 객체는 spread 문법으로 복사
             //tableData[action.row] = [...tableData[action.row]];
             // tableData[action.row][action.cell] = state.turn;
             tableData, 
             recentCell: [action.row, action.cell], //최근 클릭한 셀, 아래 빙고 확인 위해 추가
         }
        }
         case CHANGE_TURN: { //턴 O X 교체
          return {
              ...state, 
              turn: state.turn === 'O'? 'X': 'O',
          }
        }
        case RESET_GAME: { //초기화
          return {
            ...state, 
            turn: 'O',
            tableData: [['','',''], ['','',''], ['','','']],
            recentCell: [-1, -1],
          };
        }
    default: 
        return state;
    }
}

const Tictactoe = () => {  
    const [state, dispatch] = useReducer(reducer, initialState); 
    const {tableData, turn , winner, recentCell} = state;

    useEffect(() => { //빙고 확인 코드-8가지 경우의 수 체크
        //셀 클릭 -> reducer의 CLICK_CELL 실행 -> 클릭한 셀을 recentCell에 저장 
        //-> recentCell이 바뀔때마다 useEffect 작동
        const [row, cell] = recentCell;
        if(row < 0) {
            return;
        }
        let win = false; //승자 없음
        if(tableData[row][0] === turn && tableData[row][1] === turn && tableData[row][2] === turn) {
            win = true; //가로줄 검사
        }
        if(tableData[0][cell] === turn && tableData[1][cell] === turn && tableData[2][cell] === turn) {
            win = true; //세로줄 검사
        }
        if(tableData[0][0] === turn && tableData[1][1] === turn && tableData[2][2] === turn) {
            win = true; //대각선 검사
        }
        if(tableData[0][2] === turn && tableData[1][1] === turn && tableData[2][0] === turn) {
            win = true; //대각선 검사
        }
        console.log(win, row, cell, turn);
        if(win) { //이겼을때(빙고)
            dispatch({type: SET_WINNER, winner: turn}); //현재 turn이 승자
            dispatch({type: RESET_GAME}); //초기화
        }else{ //무승부 검사
            let all = true; //모든 칸 참(가정) -> 무승부 의미
            tableData.forEach((row) => {row.forEach((cell) => { //이차원 배열 forEach()
                if(!cell) { //하나라도 안찼으면
                    all = false; //false로 변경
                }
            });
        });
            if(all){ //무승부
                dispatch({type: RESET_GAME});
            }else{
                dispatch({type: CHANGE_TURN}); //턴 넘기기
            }
        }
    }, [recentCell]); //recentCell가 바뀔때마다 실행
        
    return (
        <div>
            {/* <Table onClick={onClickTable} tableData={tableData} dispatch={dispatch} /> */}
            <Table tableData={tableData} dispatch={dispatch} />
            {winner && <div>{winner}님의 승리입니다.</div>}
        </div>
    );
}

export default Tictactoe;

 

Table.jsx

import React from 'react';
import Tr from './Tr';

const Table = ({onClick, tableData, dispatch}) => {
    return (
        <table onClick={onClick}>
            <tbody>
            {Array(tableData.length).fill().map( (tr, i) => 
            (<Tr key={i} rowIndex = {i} rowData = {tableData[i]} dispatch={dispatch} />))} 
            {/* rowData = ['','',''], tableData를 rowData로 넘김, i = 줄번호 */}
           </tbody>
        </table>
    );
}

export default Table;

 

Tr.jsx

import React, {useMemo} from 'react';
import Td from './Td';

const Tr = ({rowData, rowIndex, dispatch}) => {
    
    return (
        <tr>
            {/* 클릭한 td의 위치를 알기 위해 필요 -> rowIndex: 줄수, cellIndex: 칸수 */}
            {Array(rowData.length).fill().map( (v, i) => 
            useMemo(
                () => <Td key={i} dispatch={dispatch} rowIndex={rowIndex} cellIndex={i} 
                cellData={rowData[i]}/>,
                [rowData[i]]
            )
            )}
            {/* rowData를 cellData로 Td에 보내줌 */}
        </tr>
    );
};

export default Tr;

 

Td.jsx

import React, {useCallback} from 'react';
import {CLICK_CELL} from './Tictactoe'

const Td = ({rowIndex, cellIndex, dispatch, cellData}) => { //dispatch를 Tictactoe부터 Td까지 props로 넘김
    const onClickTd = useCallback( () => {
        //console.log(rowIndex, cellIndex); //몇번째 줄, 칸의 위치를 알수 있음
        if(cellData) { //이미 클릭했을 경우, 재클릭 방지
            return;
        }
        //CLICK_CELL action dispatch, 클릭한 td의 위치 전달(row, cell)
        dispatch({type: CLICK_CELL, row: rowIndex, cell: cellIndex}); 
    }, [cellData]); //cellData를 바뀌는 값

    return (
        <td onClick={onClickTd}>{cellData}</td>
    );
}

export default Td;
반응형

댓글