과거 프로그래밍 자료들/React
[웹 게임을 만들며 배우는 React] - 틱택토, useReducer, useCallback
평부
2022. 9. 29. 22:59
출처: https://www.inflearn.com/course/web-game-react/dashboard
* 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;
[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;