먼저 1빙고하는 사람이 이기는 게임이다. 한줄 판별, 유저 변경, 모달창 구현을 하였다. 목업 툴을 통해 대략적으로 구상해본 화면이다.
initialState
로 설정된 tableData
를 테이블로 출력하기.
1️⃣ tableData
를 table
에 props
로 전달
//MineSearch.tsx
interface ReducerState {
...
tableData: string[][];
...
}
const initialState: ReducerState = {
...
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
};
<Table
onClick={onClickTable}
tableData={tableData}
dispatch={dispatch}
/>
2️⃣ tableData
길이만큼 테이블로 생성.
// Table.tsx
const Table = ({ tableData, dispatch }: Props) => {
return (
<table>
{Array(tableData.length)
.fill(null)
.map((tr, i) =>
useMemo(
() => (
<Tr
key={i}
dispatch={dispatch}
rowIndex={i}
rowData={tableData[i]}
/>
),
[tableData[i]]
)
)}
</table>
);
};
export default Table;
3️⃣ 테이블 가로길이 생성.
// Tr.tsx
const Tr = ({ rowData, rowIndex, dispatch }: Props) => {
console.log('tr rendered');
return (
<tr>
{Array(rowData.length)
.fill(null)
.map((td, i) =>
useMemo(
() => (
<Td
key={i}
dispatch={dispatch}
rowIndex={rowIndex}
cellIndex={i}
cellData={rowData[i]}
>
{''}
</Td>
),
[rowData[i]]
)
)}
</tr>
);
};
export default memo(Tr);
4️⃣ 테이블 세로길이 생성.
// Td.tsx
const Td = ({ rowIndex, cellIndex, dispatch, cellData }: Props) => {
console.log('td rendered');
const onClickTd = useCallback(() => {
console.log(rowIndex, cellIndex);
if (cellData) {
return;
}
dispatch(clickCell(rowIndex, cellIndex));
}, [cellData]);
return (
<td onClick={onClickTd}>
{cellData === 'O' && (
<div>{cellData}</div>
)}
{cellData === 'X' && (
<div>{cellData}</div>
)}
</td>
);
};
export default memo(Td);
1️⃣ 만약 이미 칸에 값이 존재한다면 return. 즉, 이미 클릭한 칸이라면 return.
2️⃣ clickCell
라는 action
이 dispatch
// Td.tsx
...
const onClickTd = useCallback(() => {
if (cellData) return; // 1️⃣ 번
dispatch(clickCell(rowIndex, cellIndex)); // 2️⃣ 번
}, [cellData]);
...
<td onClick={onClickTd}>...</td>
3️⃣ 기존의 테이블의 row
, cell
값은 클릭되어 변경된 row
, cell
값으로 변경
4️⃣ recentCell
값이 업데이트.
// TicTacToe.tsx
export const CLICK_CELL = 'CLICK_CELL' as const;
...
interface ClickCellAction {
type: typeof CLICK_CELL;
row: number;
cell: number;
}
...
export const clickCell = (row: number, cell: number): ClickCellAction => {
return { type: CLICK_CELL, row, cell };
};
...
const reducer = (state: ReducerState, action: ReducerActions): ReducerState => {
case CLICK_CELL: {
// 3️⃣ 번
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], // 4️⃣ 번
};
}
}
1️⃣ recentCell
의 값 변경에 따라 CHANGE_TURN
dispatch
.
// TicTacToe.tsx
useEffect(() => {
const [row, cell] = recentCell;
...
else {
dispatch({ type: CHANGE_TURN });
}
}
}, [recentCell]);
2️⃣ 값이 O
였다면 X
로, 그렇지 않고 X
였다면 O
로 변경.
// TicTacToe.tsx
export const CHANGE_TURN = 'CHANGE_TURN' as const;
interface ChangeTurnAction {
type: typeof CHANGE_TURN;
}
const reducer = (state: ReducerState, action: ReducerActions): ReducerState => {
case CHANGE_TURN: {
return {
...state,
turn: state.turn === 'O' ? 'X' : 'O', // 2️⃣ 번
};
}
}
1️⃣ 한줄이 모두 같은 값으로 일치한다면 win은 true. 즉, 승자가 나온 것.
2️⃣ 승리시 승자를 판별하는 SET_WINNER
dispatch
. 게임을 다시 리셋하는 RESET_GAME
dispatch
.
3️⃣ 만약 한줄을 만족하는 값이 존재하지 않을 경우 무승부로 판별.
useEffect(() => {
...
let win = false;
if ( // 1️⃣ 번
tableData[row][0] === turn &&
tableData[row][1] === turn &&
tableData[row][2] === turn
) {
win = true;
}
if ( // 1️⃣ 번
tableData[0][cell] === turn &&
tableData[1][cell] === turn &&
tableData[2][cell] === turn
) {
win = true;
}
if ( // 1️⃣ 번
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) { // 2️⃣ 번
// 승리시
dispatch({ type: SET_WINNER, winner: turn });
dispatch({ type: RESET_GAME });
} else {
let all = true; // all이 true면 무승부
tableData.forEach(row => {
// 무승부 검사
row.forEach(cell => {
if (!cell) {
all = false;
}
});
});
if (all) { // 3️⃣ 번
dispatch({ type: RESET_GAME });
dispatch(setWinner('무승부'));
} else {
dispatch({ type: CHANGE_TURN });
}
}
}, [recentCell]);
위에서 dispatch
되며 winner
에 전달받은 turn값
이 O
또는 X
가 저장.
export const SET_WINNER = 'SET_WINNER';
interface SetWinnerAction {
type: typeof SET_WINNER;
winner: 'O' | 'X' | '무승부';
}
const setWinner = (winner: 'O' | 'X' | '무승부'): SetWinnerAction => {
return { type: SET_WINNER, winner };
};
const reducer = (state: ReducerState, action: ReducerActions): ReducerState => {
...
case SET_WINNER:
return {
...state,
winner: action.winner,
};
}
const TicTacToe = () => {
const [state, dispatch] = useReducer<Reducer<ReducerState, ReducerActions>>(
reducer,
initialState
);
const { tableData, turn, winner, recentCell } = state;
return ( // reducer에 저장된 winner값을 모달창에 전달하여 winner 출력.
{winner && <Modal tictactoe={'틱텍토'} tictactoeWinner={winner} />}
)
}