관리 메뉴

프로그래밍 삽질 중

[웹 게임을 만들며 배우는 React] - 틱택토, useReducer, useCallback 본문

과거 프로그래밍 자료들/React

[웹 게임을 만들며 배우는 React] - 틱택토, useReducer, useCallback

평부 2022. 9. 29. 22:59

 

 

출처: https://www.inflearn.com/course/web-game-react/dashboard

 

[무료] 웹 게임을 만들며 배우는 React - 인프런 | 강의

웹게임을 통해 리액트를 배워봅니다. Class, Hooks를 모두 익히며, Context API와 React Router, 웹팩, 바벨까지 추가로 배웁니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

reducer, dispatch 관련 설명

 

 

* useReducer란?

- state가 많아지면 변수들이 많아짐 → 한 번에 모아서 관리하면 어떨까?

▶ 이 기능을 담당하는 것이 useReducer

 

 

* (1) state를 모아서 action을 통해 바꿈, (2) action을 dispatch하면 action이 실행됨

▶ (3) reducer에서 정의한대로 state를 바꿈

state를 바꿀 때는 불변성이 항상 중요함 

 

//(1) state를 하나로 모아둠
const initialState = {
  winner: "",
  turn: "O",
  tableData: [
    ["", "", ""],
    ["", "", ""],
    ["", "", ""],
  ],
  recentCell: [-1, -1],
};

//(2) action을 통해서만 바꿈
dispatch({ type: SET_WINNER, winner: "O" });
dispatch({ type: SET_WINNER, winner: turn });
dispatch({ type: RESET_GAME });

//(3) reducer에서 정의한대로 state를 바꿈
const reducer = (state, action) => {
  switch (action.type) {
    case SET_WINNER:
      //state.winner = action.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;
  }
};

 

 

* useCallback 

▶ 왜 사용? = 함수 onClickTd를 props로 넣을 때마다 불필요한 렌더링이 발생 → 함수를 기억할 필요 있음

▶ 아래의 예시는 cellData가 바뀔때마다 함수 초기화함

▶ 처음 데이터를 감지하고 바뀌는 데이터를 감지를 못하기 때문에 바뀔 여지가 있는 데이터(cellData)를 []에 넣음

import React, { useCallback } from "react";
import { CLICK_CELL } from "./T3";

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;
  
  /* {
    console.log(rowIndex, cellIndex);
    if (cellData) {
      //한 번이상 못 누르게 하기
      return;
    }
    dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });
  }
 이 부분 기억함 
 */

 

[Td.jsx]

▶ 칸만 눌렀는데 칸 전체가 렌더링됨(문제 있음)
▶ 렌더링이 무엇이 됬는지 파악(useEffect, useRef 사용)

▶ props 자체의 문제는 아님

 

▶ memo로 해결(memo가 되지 않으면 useMemo(컴포넌트 자체를 기억)도 고려) = 클릭하는 일부만 리렌더링됨

import React, { useCallback, useEffect, useRef, memo } from "react";
import { CLICK_CELL } from "./T3";

const Td = memo(({ rowIndex, cellIndex, dispatch, cellData }) => {
  const ref = useRef([]);
  useEffect(() => {
    console.log(
      rowIndex === ref.current[0],
      cellIndex === ref.current[1],
      dispatch === ref.current[2],
      cellData === ref.current[3]
    ); //cellData가 바뀐 것으로 나옴
    console.log("cellData", cellData, ref.current[3]);
    ref.current = [rowIndex, cellIndex, dispatch, cellData];
  }, [rowIndex, cellIndex, dispatch, cellData]); //모든 props 다 넣기
  const onClickTd = useCallback(() => {
    console.log("td rendered");

    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;

td rendered가 정상적으로 작동

 

 

[Tr.jsx]

▶ 렌더링이 무엇이 됬는지 파악(useEffect, useRef 사용)

▶ props 자체의 문제는 아님

 

▶ useMemo 사용 : 값을 기억, 컴포넌트를 기억하는 것도 가능

▶ 셀을 갱신 : rowData[i]가 바뀌었을 때

import React, { useRef, useEffect, memo } from "react";
import Td from "./Td";

const Tr = memo(({ rowData, rowIndex, dispatch }) => {
  console.log("tr rendered");
  const ref = useRef([]);
  useEffect(() => {
    console.log(
      rowData === ref.current[0],
      rowIndex === ref.current[1],
      dispatch === ref.current[2]
    );
    ref.current = [rowData, rowIndex, dispatch];
  }, [rowData, rowIndex, dispatch]); //모든 props 다 넣기
  return (
    <tr>
      {Array(rowData.length)
        .fill()
        .map((td, i) => (
          <Td
            key={i}
            cellIndex={i}
            rowIndex={rowIndex}
            cellData={rowData[i]}
            dispatch={dispatch}
          >
            {""}
          </Td>
        ))}
    </tr>
  );
});

export default Tr;

 

 

[Table.jsx]

import React, { memo } from "react";
import Tr from "./Tr";

const Table = memo(({ tableData, dispatch }) => {
  return (
    //i가 몇 번째 줄인지 나타냄
    <table>
      {Array(tableData.length)
        .fill()
        .map((tr, i) => (
          <Tr key={i} dispatch={dispatch} rowIndex={i} rowData={tableData[i]} />
        ))}
    </table>
  );
});

export default Table;

 

 

[T3.jsx]

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

const initialState = {
  //state를 하나로 모아둠
  winner: "",
  turn: "O",
  tableData: [
    ["", "", ""],
    ["", "", ""],
    ["", "", ""],
  ],
  recentCell: [-1, -1],
};

export const SET_WINNER = "SET_WINNER";
export const CLICK_CELL = "CLICK_CELL"; //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 -> 이렇게 직접 바꾸면 안 됨
      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 T3 = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { tableData, turn, winner, recentCell } = state;
  const onClickTable = useCallback(() => {
    dispatch({ type: SET_WINNER, winner: "O" }); //액션을 통해 바꿈
    //액션만 있다고 자동으로 state가 바뀌는 것이 아님
    //reducer = 액션을 해석해서 state를 직접 바꿈
  }, []);

  //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, 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 T3;