state가 많아지면 관리가 힘들어지기 때문에 useReducer
를 사용하여 하나의 state, setState 로 통일할 수 있음
const [state, dispatch] = useReducer(reducer, initialState);
reducer
: state와 action객체를 파라미터로 받아와 새로운 상태로 반환해주는 함수action
: 업데이트를 위한 정보를 가지고 있음. 이름은 대문자로 보통 사용dispatch
: 액션을 실행시키는 함수import React, { useState, useReducer, useCallback } from 'react';
import Table from './Table';
const initialState = { // state들 여기에 묶어주기
winner: '',
turn: 'O',
tableData: [['', '', ''], ['', '', ''], ['', '', '']],
};
const SET_WINNER = 'SET_WINNER';
const reducer = (state, action) => { // action을 해석 후 state를 바꿔주는 역할
switch (action.type) {
case 'SET_WINNER':
return { // return으로 새로운 상태 반환
...state, // 기존 state를 직접 바꾸지 않고 얕은 복사 후 바뀌는 부분만 넣기
winner: action.winner,
}
};
};
const TicTacToe = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const onClickTable = useCallback( // 컴포넌트에 들어가는 이벤트 함수는 useCallback 써주기 (함수 재사용)
() => {
dispatch({ type: SET_WINNER, winner: 'O' }); // dispatch안에 들어가는 객체를 action이라고 함
}, []);
return (
<>
<Table onClick={onClickTable} tableData={state.tableData} />
{state.winner && <div>{state.winner}님의 승리!</div>}
</>
);
};
export default TicTacToe;
클릭한 곳이 몇 번째 줄, 몇 번째 칸인지 알아내고 클릭할 때마다 턴 바꾸기
import React, { useState, useReducer, useCallback } from 'react';
import Table from './Table';
const initialState = {...};
export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';
const reducer = (state, action) => {
switch (action.type) {
case SET_WINNER:
return {...};
case CLICK_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...tableData[action.row]];
tableData[action.row][action.cell] = state.turn;
return {
...state,
tableData,
};
}
case CHANGE_TURN: {
return {
...state,
turn: state.turn === 'O' ? 'X' : 'O',
// 기존 턴이 'O'면 'X'로 'X'면 'O'로
};
}
}
};
const TicTacToe = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const onClickTable = useCallback(
() => {
dispatch({ type: SET_WINNER, winner: 'O' });
}, []);
return (
<>
<Table onClick={onClickTable} tableData={state.tableData} dispatch={dispatch} />
{state.winner && <div>{state.winner}님의 승리!</div>}
</>
);
};
export default TicTacToe;
import React from 'react';
import Tr from './Tr';
const Table = ({ onClick, tableData, dispatch }) => {
return (
<table>
{Array(tableData.length).fill().map((tr, i) => (<Tr dispatch={dispatch} rowIndex={i} rowData={tableData[i]} />))}
</table>
);
};
export default Table;
import React from 'react';
import Td from './Td';
const Tr = ({ rowData, rowIndex, dispatch }) => {
return (
<tr>
{Array(rowData.length).fill().map((td, i) => (
<Td dispatch={dispatch} rowIndex={rowIndex} cellIndex={i} cellData={rowData[i]}>{''}</Td>
))}
</tr>
);
};
export default Tr;
import React, { useCallback } from 'react';
import { CLICK_CELL, CHANGE_TURN } from './TicTacToe';
const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
const onClickTd = useCallback(() => {
console.log(rowIndex, cellIndex);
dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex }); // 칸 클릭 후
dispatch({ type: CHANGE_TURN }); // 턴 바꾸기
}, []);
return (
<td onClick={onClickTd}>{cellData}</td>
);
};
export default Td;
한 번 눌렀던 칸은 못 누르게 하고, 승자 & 무승부 가리기
import React, { useCallback } from 'react';
import { CLICK_CELL } from './TicTacToe';
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;
import React, { useEffect, useReducer, useCallback } from 'react';
import Table from './Table';
const initialState = {
winner: '',
turn: 'O',
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
recentCell: [-1, -1], // 없는 칸 임의로 설정
};
export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';
export const RESET_GAME = 'RESET_GAME';
const reducer = (state, action) => {
switch (action.type) {
case SET_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 TicTacToe = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { tableData, turn, winner, recentCell } = state;
const onClickTable = useCallback(
() => {
dispatch({ type: SET_WINNER, winner: 'O' });
}, []);
useEffect(() => {
const [row, cell] = recentCell;
if (row < 0) { // useEffect는 첫 랜더링때도 실행되기 때문에 걸러주기
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 TicTacToe;
❗ useReducer
에선 state가 비동기적이기 때문에 useEffect
사용
React.memo()
, useMemo()
사용하여 최적화하기
import React, { useCallback, memo } from 'react';
import { CLICK_CELL } from './TicTacToe';
const Td = memo(({ 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;
import React, { memo, useMemo } from 'react';
import Td from './Td';
const Tr = memo(({ rowData, rowIndex, dispatch }) => {
return (
<tr>
{Array(rowData.length).fill().map((td, i) => (
useMemo(() => <Td key={i} dispatch={dispatch} rowIndex={rowIndex} cellIndex={i} cellData={rowData[i]}>{''}</Td>,
[rowData[i]])
))}
</tr>
);
});
export default Tr;
import React, { useMemo } from 'react';
import Tr from './Tr';
const Table = ({ onClick, tableData, dispatch }) => {
return (
<table>
{Array(tableData.length).fill().map((tr, i) => (
useMemo(
() => <Tr key={i} dispatch={dispatch} rowIndex={i} rowData={tableData[i]} />,
[tableData[i]]
)
))}
</table>
);
};
export default Table;