본문 바로가기
카테고리 없음

[React.js] 제로초 웹게임 - 7. 틱택토 (useReducer, memo)

by Chaedie 2022. 12. 1.
728x90

강의를 통해 배운 점

useReducer를 사용해 state를 한곳에서 관리 하는 방법을 배웠다.

useReducer 자체가 redux의 스토어와 같은 역할을 하고, action 객체를 dispatch를 통해 실행신다는 개념 또한 리덕스의 Flux 패턴을 채용했다고 한다.

정석적인 공부 로드맵에 비해 useReducer를 배우는 타이밍이 많이 늦었는데, 그래도 좋은 강의 덕에 잘 살펴보아서 좋았다.


useReducer 사용

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

const initialState = {
  winner: '',
  turn: 'O',
  tableData: [
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ],
};

const reducer = () => {};

function TicTacToe() {
  const [state, dispatch] = useReducer(reducer, initialState);
  // const [winner, setWinner] = useState('');
  // const [turn, setTurn] = useState('O');
  // const [tableData, setTableData] = useState(['', '', ''], ['', '', ''], ['', '', '']);

  return (
    <>
      <Table></Table>
      {winner && <div>틱택토</div>}
    </>
  );
}

export default TicTacToe;

1) state가 3개로 나뉘어져 있는 상황

2) <Table/> ⇒ <Tr/> ⇒ <Td/> 로 컴포넌트가 연결된 상황

이기에 useReducer를 통해 1) 하나의 스테이트처럼 사용하여 state를 관리 하고, 2) 전역 상태관리를 하도록 한다.


reducer

reducer에서 action 객체를 통해 들어온 모든 동작을 관리한다. 지금 느끼기엔 조금 복잡하게 느껴지지만 한곳에서 관리한다는데 의의를 둔다고 하니 일단은 익숙해져야겠다.

지금은 동작이 간단하지만 프로젝트가 커져서 로직이 복잡해지고 많아진다면 이 또한 분리를 잘 해야 할 것 같다.

import React, { useCallback, useEffect, useReducer } 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.rowIndex] = [...tableData[action.rowIndex]];
      tableData[action.rowIndex][action.cellIndex] = state.turn;
      return {
        ...state,
        tableData,
        recentCell: [action.rowIndex, action.cellIndex],
      };
    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;
  }
};

function 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;
    let win;
    if (row < 0) {
      return;
    }
    if (tableData[row].every(x => x === turn)) {
      win = true;
    }
    if ([tableData[0][cell], tableData[1][cell], tableData[2][cell]].every(x => x === turn)) {
      win = true;
    }
    if ([tableData[0][0], tableData[1][1], tableData[2][2]].every(x => x === turn)) {
      win = true;
    }
    if ([tableData[0][2], tableData[1][1], tableData[2][0]].every(x => x === turn)) {
      win = true;
    }
    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: SET_WINNER, winner: null });
        dispatch({ type: RESET_GAME });
      } else {
        dispatch({ type: CHANGE_TURN });
      }
    }
  }, [recentCell]);

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

export default TicTacToe;

댓글