TIL 22-04-20

56992 단어 ReactReact

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


React Lecture CH 7

1 - useReducer
2 -reducer, action, dispatch
3 - action 만들어 dispatch

4 - 틱택토 게임
5 - 테이블 최적화

  • 지난 시간 결과

🙋‍♂️ 해결할 것
1. 한번 누른 곳은 다시 클릭 못하게 막기
2. 이긴 사람 정하는 로직 구현 (winner)
+무승부인 경우도 판단

틱택토 게임 완성

1. 한번 누른 셀을 못누르게

Td.jsx

const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
  const onClickTd = useCallback(() => {
    if (cellData) {
      return; // 셀 데이터가 존재하면 빠져나옴 -> 한번 클릭한 셀은 변경되지 않게
    }
    dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });
    dispatch({ type: CHANGE_TURN });
  }, [cellData]);

  return <td onClick={onClickTd}>{cellData}</td>;
};
  • 간단하게 if문만 추가하면 된다.
  • if (cellData) break;
  • 처음 클릭하는 곳이면 cellData가 ''이므로 false일 것이다.
  • 만약 이미 클릭한 곳이면, cellData는 'O' or 'X'이므로 true가 되어 if문을 만족한다.
    -> break로 onClickTd 함수를 빠져나간다.

✅ 참고 - cellData와 tableData

참고 2 - state는 비동기이다!
cf) 리덕스(Redux)는 동기적으로 바뀜

-> 비동기 state를 처리해주려면 useEffect를 써야함.


2. 승자(winner) 판단 로직

O 또는 X가 승리하는 경우는 크게 4가지이다.

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;
    }

비동기적으로 tableData가 변경되므로, 우리는 useState를 이용해야 한다.

TicTacToe.jsx

const reducer = (state, action) => {
  switch (action.type) {
   // 생략
    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], // 👈 추가
      };

현재 셀의 위치를 배열로 나타내는 state인 recentCell을 추가하고,
reducer에서 CLICK_CELL이 실행될때마다 현재 셀 위치를 저장함.

const initialState = {
  winner: '',
  turn: 'O',
  tableData: [
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ],
  recentCell: [-1, -1], // 👈 추가
};
  • initialState에 recentCell을 추가해줌.
    (초기값은 -1, 즉 없는 인덱스로)
// useEffect
useEffect(() => {}, [recentCell]);
useEffect(() => {
    const [row, cell] = recentCell;
    if (row < 0) {
      return;
      // useEffect는 첫 렌더링시에도 실행되는데, 이를 막기 위함.
    }
    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;
    }
    if (win) {
      dispatch({ type: SET_WINNER, winner: turn });
    } else {
      // 무승부 검사
    }
  }, [recentCell]);

useEffect 수정

  1. useEffect는 첫 렌더링시에도 실행됨.
    -> if문을 걸어서 실행되지 않게.
  • recentCell의 row,cell은 초기값이 -1이므로
    if (row < 0)를 조건으로 걸면 됨.

  1. 승리 조건 win이 true면
    -> dispatch({ type: SET_WINNER, winner: turn });

  1. win이 false면
    -> 1) 무승부 검사 (테이블이 다 찼는지)
    -> 2) 다음 턴으로 넘김(O에서 X차례로)

1) 무승부 검사

let all = true;
      tableData.forEach((row) => {
        row.forEach((cell) => {
          if (!cell) {
            all = false;
          }
        });
      });
  • forEach를 중첩해서 row, cell별로 다 순회하며
    cell이 비어있는지 채크.
  • 하나라도 비어있다면 (= if (!cell)이면) all은 false임

1-2) 무승부면 = 즉 테이블이 다 찼으면 (all=true)

if (all) {
        // 무승부면 리셋
        dispatch({ type: SET_WINNER, winner: null });
        dispatch({ type: RESET_GAME });
      } 

2) 무승부가 아니면 다음 턴으로 넘김

else {
        // 무승부 아니면 턴 넘김
        dispatch({ type: CHANGE_TURN });
      }

🔻 useEffect 전체 코드

useEffect(() => {
    const [row, cell] = recentCell;
    if (row < 0) {
      return;
      // useEffect는 첫 렌더링시에도 실행되는데, 이를 막기 위함.
    }
    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;
    }
    if (win) {
      // 승리시
      dispatch({ type: SET_WINNER, winner: turn });
      dispatch({ type: RESET_GAME });
    } else {
      // 무승부 검사 - 칸이 다 차있으면 (즉, all이 true면) 무승부임
      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]);

<결과>


성능 최적화

Chrome Dev Tool (React Dev Tool)을 이용해서
렌더링 되는 부분을 살펴보면,

분명 한 셀만 클릭해서 바뀌면 되는데, 하나가 바뀔때마다 전체가 렌더링 되고 있다.

-> 성능 최적화를 위해 useEffect와 useRef를 사용해서 알아보자.

useRef, useEffect

  • 무엇이 리렌더링을 유발하는지 모르겠을때 사용하면 좋다.

Td.jsx


const Td = ({ 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]
    );
    ref.current = [rowIndex, cellIndex, dispatch, cellData];
  }, [rowIndex, cellIndex, dispatch, cellData]);
	// 모든 props를 다 적어줌
  ...
}
  • ref.current에 모든 props를 배열 형태로 저장하고, 저장한 값과 같은지 비교한다.
  • 여기서 false가 나오면 값이 바뀌었다는 뜻이고,
    그것때문에 리렌더링이 발생하고 있는 것.

성능 최적화시 많이 사용하는 방식이다.
(useRef + useEffect로 비교)

-> cellData 가 바뀌어서 리렌더링이 되고 있었던 것.


console.log를 찍어본 결과 Td 자체에는 원하는 것만 바뀌고 있음.
이럴땐 React.memo로 PureComponent 처럼 바꿔주면 됨.

React.memo 사용

  • Tr, Td 컴포넌트를 모두 React.memo로 감싼다.

Td.jsx

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

const Td = memo(({ rowIndex, cellIndex, dispatch, cellData }) => {
  console.log('Td render');

  const onClickTd = useCallback(() => {
    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 } from 'react';
import Td from './Td';

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

export default Tr;

td 클릭시

  • td가 리렌더링 -> tr이 리렌더링 -> table이 리렌더링 됨.
    (자식 리렌더링시 부모가 리렌더링 되는 것처럼 보임)
  • 실제로는 td만 리렌더링 되는 것.
* * *

+) React.useMemo 사용시

  • React.useMemo는 함수의 값을 기억하는 것 이지만, 컴포넌트를 기억할수도 있다.
import React, { useMemo } from 'react';
import Td from './Td';

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

export default Tr;
  • rowData[i]가 바뀔때만 다시 리렌더링 됨.
    (함수의 값을 다시 기억하는것)

🙋‍♀️ 만약 React.memo()로 감싸줬는데도 리렌더링이 된다면?
최후의 수단으로 React.useMemo()를 사용하자.
-> 컴포넌트 자체를 기억해버림.

좋은 웹페이지 즐겨찾기