TIL 22-04-20
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 수정
- useEffect는 첫 렌더링시에도 실행됨.
-> if문을 걸어서 실행되지 않게.
- recentCell의 row,cell은 초기값이 -1이므로
if (row < 0)를 조건으로 걸면 됨.
- 승리 조건 win이 true면
-> dispatch({ type: SET_WINNER, winner: turn });
- 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()를 사용하자.
-> 컴포넌트 자체를 기억해버림.
Author And Source
이 문제에 관하여(TIL 22-04-20), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@thisisyjin/TIL-22-04-19-2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)