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)
+무승부인 경우도 판단
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>;
};
✅ 참고 - cellData와 tableData
참고 2 - state는 비동기이다!
cf) 리덕스(Redux)는 동기적으로 바뀜-> 비동기 state를 처리해주려면
useEffect
를 써야함.
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], // 👈 추가
};
// 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]);
1) 무승부 검사
let all = true;
tableData.forEach((row) => {
row.forEach((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(() => {
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를 사용해서 알아보자.
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를 다 적어줌
...
}
성능 최적화시 많이 사용하는 방식이다.
(useRef + useEffect로 비교)
-> cellData
가 바뀌어서 리렌더링이 되고 있었던 것.
console.log를 찍어본 결과 Td 자체에는 원하는 것만 바뀌고 있음.
이럴땐 React.memo로 PureComponent 처럼 바꿔주면 됨.
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만 리렌더링 되는 것.
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;
🙋♀️ 만약 React.memo()로 감싸줬는데도 리렌더링이 된다면?
최후의 수단으로 React.useMemo()를 사용하자.
-> 컴포넌트 자체를 기억해버림.