[React] 웹 게임-7 틱택토

ji_silver·2020년 8월 14일
0

[React] 웹 게임

목록 보기
7/8
post-thumbnail

1. useReducer 소개

state가 많아지면 관리가 힘들어지기 때문에 useReducer를 사용하여 하나의 state, setState 로 통일할 수 있음

const [state, dispatch] = useReducer(reducer, initialState);

2. reducer, action, dispatch의 관계

  • reducer: state와 action객체를 파라미터로 받아와 새로운 상태로 반환해주는 함수
  • action: 업데이트를 위한 정보를 가지고 있음. 이름은 대문자로 보통 사용
  • dispatch: 액션을 실행시키는 함수

TicTacToe.jsx

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

const initialState = { // state들 여기에 묶어주기
    winner: '',
    turn: 'O',
    tableData: [['', '', ''], ['', '', ''], ['', '', '']],
};

const SET_WINNER = 'SET_WINNER';

const reducer = (state, action) => { // action을 해석 후 state를 바꿔주는 역할
    switch (action.type) {
        case 'SET_WINNER':
            return { // return으로 새로운 상태 반환
                ...state, // 기존 state를 직접 바꾸지 않고 얕은 복사 후 바뀌는 부분만 넣기
                winner: action.winner,
            }
    };
};

const TicTacToe = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const onClickTable = useCallback( // 컴포넌트에 들어가는 이벤트 함수는 useCallback 써주기 (함수 재사용)
        () => {
            dispatch({ type: SET_WINNER, winner: 'O' }); // dispatch안에 들어가는 객체를 action이라고 함
        }, []);

    return (
        <>
            <Table onClick={onClickTable} tableData={state.tableData} />
            {state.winner && <div>{state.winner}님의 승리!</div>}
        </>
    );
};

export default TicTacToe;

3. action 만들어 dispatch 하기

클릭한 곳이 몇 번째 줄, 몇 번째 칸인지 알아내고 클릭할 때마다 턴 바꾸기

TicTacToe.jsx

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

const initialState = {...};

export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';

const reducer = (state, action) => {
    switch (action.type) {
        case SET_WINNER:
            return {...};
                    
        case CLICK_CELL: {
            const tableData = [...state.tableData];
            tableData[action.row] = [...tableData[action.row]];
            tableData[action.row][action.cell] = state.turn;
            return {
                ...state,
                tableData,
            };
        }
        case CHANGE_TURN: {
            return {
                ...state,
                turn: state.turn === 'O' ? 'X' : 'O',
              	// 기존 턴이 'O'면 'X'로 'X'면 'O'로
            };
        }
    }
};

const TicTacToe = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const onClickTable = useCallback(
        () => {
            dispatch({ type: SET_WINNER, winner: 'O' });
        }, []);

    return (
        <>
            <Table onClick={onClickTable} tableData={state.tableData} dispatch={dispatch} />
            {state.winner && <div>{state.winner}님의 승리!</div>}
        </>
    );
};

export default TicTacToe;

Table.jsx

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

const Table = ({ onClick, tableData, dispatch }) => {
    return (
        <table>
            {Array(tableData.length).fill().map((tr, i) => (<Tr dispatch={dispatch} rowIndex={i} rowData={tableData[i]} />))}
        </table>
    );
};

export default Table;

Tr.jsx

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

const Tr = ({ rowData, rowIndex, dispatch }) => {
    return (
        <tr>
            {Array(rowData.length).fill().map((td, i) => (
                <Td dispatch={dispatch} rowIndex={rowIndex} cellIndex={i} cellData={rowData[i]}>{''}</Td>
            ))}
        </tr>
    );
};

export default Tr;

Td.jsx

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

const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
    const onClickTd = useCallback(() => {
        console.log(rowIndex, cellIndex);
        dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex }); // 칸 클릭 후
        dispatch({ type: CHANGE_TURN }); // 턴 바꾸기
    }, []);

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

export default Td;

4. 틱택토 구현하기

한 번 눌렀던 칸은 못 누르게 하고, 승자 & 무승부 가리기

Td.jsx

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

const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
    const onClickTd = useCallback(() => {
        console.log(rowIndex, cellIndex);
        if (cellData) { // 기존 데이터가 있으면 리턴
            return;
        }
        dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });
    }, [cellData]);

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

export default Td;

TicTacToe.jsx

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

const initialState = {
    winner: '',
    turn: 'O',
    tableData: [
        ['', '', ''],
        ['', '', ''],
        ['', '', ''],
    ],
    recentCell: [-1, -1], // 없는 칸 임의로 설정
};

export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';
export const RESET_GAME = 'RESET_GAME';

const reducer = (state, action) => {
    switch (action.type) {
        case SET_WINNER:
            return {
                ...state,
                winner: action.winner,
            };
        case CLICK_CELL: {
            const tableData = [...state.tableData];
            tableData[action.row] = [...tableData[action.row]];
            tableData[action.row][action.cell] = state.turn;
            return {
                ...state,
                tableData,
                recentCell: [action.row, action.cell], // 최근 클릭한 칸 기억해두기
            };
        }
        case CHANGE_TURN: {
            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;

    const onClickTable = useCallback(
        () => {
            dispatch({ type: SET_WINNER, winner: 'O' });
        }, []);

    useEffect(() => {
        const [row, cell] = recentCell;
        if (row < 0) { // useEffect는 첫 랜더링때도 실행되기 때문에 걸러주기
            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, tableData, turn);
        if (win) { // 승리시
            dispatch({ type: SET_WINNER, winner: turn });
            dispatch({ type: RESET_GAME });
        } else {
            let all = true; // 무승부
            tableData.forEach((row) => {  //무승부 검사
                row.forEach((cell) => {
                    if (!cell) {
                        all = false;
                    }
                });
            });
            if (all) {
                dispatch({ type: RESET_GAME });
            } else {
                dispatch({ type: CHANGE_TURN });
            }
        }
    }, [recentCell]);

    return (
        <>
            <Table onClick={onClickTable} tableData={tableData} dispatch={dispatch} />
            {winner && <div>{winner}님의 승리</div>}
        </>
    )
};

export default TicTacToe;

useReducer에선 state가 비동기적이기 때문에 useEffect 사용

5. 테이블 최적화하기

React.memo(), useMemo() 사용하여 최적화하기

Td.jsx

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

const Td = memo(({ rowIndex, cellIndex, dispatch, cellData }) => {
    const onClickTd = useCallback(() => {
        console.log(rowIndex, cellIndex);
        if (cellData) {
            return;
        }
        dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });
    }, [cellData]);

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

export default Td;

Tr.jsx

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

const Tr = memo(({ rowData, rowIndex, dispatch }) => {
    return (
        <tr>
            {Array(rowData.length).fill().map((td, i) => (
                useMemo(() => <Td key={i} dispatch={dispatch} rowIndex={rowIndex} cellIndex={i} cellData={rowData[i]}>{''}</Td>,
                [rowData[i]])
            ))}
        </tr>
    );
});

export default Tr;

Table.jsx

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

const Table = ({ onClick, tableData, dispatch }) => {
    return (
        <table>
            {Array(tableData.length).fill().map((tr, i) => (
                useMemo(
                    () => <Tr key={i} dispatch={dispatch} rowIndex={i} rowData={tableData[i]} />,
                    [tableData[i]]
                )
            ))}
        </table>
    );
};

export default Table;

profile
🚧개발중🚧

0개의 댓글