전역적으로 데이터가 사용되야 할 때 (TicTacToe-> Table-> Tr-> Td) 사용
createContext()
함수를 사용하여 Context 만들기value={}
안에 넣어주기import React, { useReducer, createContext, useMemo } from 'react';
export const TableContext = createContext({
// 초깃값 설정
});
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => ({ tableData: state.tableData, dispatch }), [state.tableData]); // tableDate값이 바뀔 때 갱신
return (
<TableContext.Provider value={value}>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
};
❗ Context API는 성능 최적화하기 힘들기 때문에 useMemo()
로 객체값을 기억하게 하여 캐싱 작업하기
export const CODE = {
MINE: -7, // 지뢰
NORMAL: -1, // 일반 칸
QUESTION: -2, // 물음표
FLAG :-3, // 깃발
QUESTION_MINE: -4, // 물음표 칸이 지뢰인 경우
FLAG_MINE: -5, // 깃발 칸이 지뢰인 경우
CLICKED_MINE: -6, // 지뢰 클릭
OPENED: 0, // 칸 열었을 때 (0이상이면 다 opened)
};
...
const plantMine = (row, cell, mine) => {
console.log(row, cell, mine);
const candidate = Array(row * cell).fill().map((arr, i) => { //0 ~ 99까지
return i;
});
const shuffle = [];
while(candidate.length > row * cell - mine) {
const chosen = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0];
shuffle.push(chosen); // 랜덤으로 뽑은 20개 숫자들을 shuffle 배열에 넣기
}
//2차원 배열 만들기
const data = [];
for (let i = 0; i < row; i++) {
const rowData = [];
data.push(rowData);
for (let j = 0; j < cell; j++) {
rowData.push(CODE.NORMAL); // 일반칸을 기본으로 설정
}
}
//2차원 배열에 지뢰 심기
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hor = shuffle[k] % cell;
data[ver][hor] = CODE.MINE; // 지뢰 심기
}
console.log(data);
return data;
};
export const START_GAME = 'START_GAME';
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
tableData: plantMine(action.row, action.cell, action.mine) // tableData에 지뢰가 심어짐
};
default:
return state;
}
};
import React, { useContext } from 'react';
import Tr from './Tr';
import { TableContext } from './MineSearch';
const Table = () => {
const { tableData } = useContext(TableContext);
return (
<table>
{Array(tableData.length).fill().map((tr, i) => <Tr rowIndex={i} />)}
</table>
)
};
export default Table;
import React, { useContext } from 'react';
import Td from './Td';
import { TableContext } from './MineSearch';
const Tr = ({ rowIndex }) => {
const { tableData } = useContext(TableContext);
return (
<tr>
{tableData[0] && Array(tableData[0].length).fill().map((td, i) =>
<Td rowIndex={rowIndex} cellIndex={i} />
)}
</tr>
)
};
export default Tr;
import React, { useContext } from 'react';
import { CODE, TableContext } from './MineSearch';
const getTdStyle = (code) => {
switch (code) {
case CODE.NORMAL:
case CODE.MINE:
return {
background: '#444',
};
case CODE.OPENED:
return {
background: 'white'
};
default:
return {
background: 'white'
};
}
};
const getTdText = (code) => {
switch (code) {
case CODE.NORMAL:
return '';
case CODE.MINE:
return 'X';
default:
return code || '';
}
};
const Td = ({ rowIndex, cellIndex }) => {
const { tableData } = useContext(TableContext);
return (
<td
style={getTdStyle(tableData[rowIndex][cellIndex])}
>{getTdText(tableData[rowIndex][cellIndex])}</td>
)
};
export default Td;
const Td = ({ rowIndex, cellIndex }) => {
const { tableData, dispatch, halted } = useContext(TableContext);
const onClickTd = useCallback(() => {
if (halted) {
return; // 게임이 멈추면 클릭x
}
switch (tableData[rowIndex][cellIndex]) {
//이미 연 칸, 깃발, ?칸은 그냥 return
case CODE.OPENED:
case CODE.FLAG_MINE:
case CODE.FLAG:
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return;
case CODE.NORMAL:
dispatch({ type: OPEN_CELL, row: rowIndex, cell: cellIndex });
return;
case CODE.MINE:
dispatch({ type: CLICK_MINE, row: rowIndex, cell: cellIndex });
return;
default:
return;
}
}, [tableData[rowIndex][cellIndex], halted]);
const onRightClickTd = useCallback((e) => {
e.preventDefault(); // 우클릭 시 나오는 메뉴 안 뜨게 하기
if (halted) {
return;
}
switch (tableData[rowIndex][cellIndex]) {
case CODE.NORMAL:
case CODE.MINE:
dispatch({ type: FLAG_CELL, row: rowIndex, cell: cellIndex });
return;
case CODE.FLAG_MINE:
case CODE.FLAG:
dispatch({ type: QUESTION_CELL, row: rowIndex, cell: cellIndex });
return;
case CODE.QUESTION_MINE:
case CODE.QUESTION:
dispatch({ type: NORMALIZE_CELL, row: rowIndex, cell: cellIndex });
return;
default:
return;
}
}, [tableData[rowIndex][cellIndex], halted]);
return (
<td
style={getTdStyle(tableData[rowIndex][cellIndex])}
onClick={onClickTd}
onContextMenu={onRightClickTd} // 우클릭 이벤트
>{getTdText(tableData[rowIndex][cellIndex])}</td>
)
};
export const TableContext = createContext({ // 초깃값 설정
tableData: [],
halted: true, // 게임 중단 코드
dispatch: () => { },
});
const initialState = {
tableData: [],
timer: 0,
result: '',
halted: false,
};
...
export const START_GAME = 'START_GAME';
export const OPEN_CELL = 'OPEN_CELL';
export const CLICK_MINE = 'CLICK_MINE';
export const FLAG_CELL = 'FLAG_CELL';
export const QUESTION_CELL = 'QUESTION_CELL';
export const NORMALIZE_CELL = 'NORMALIZE_CELL';
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
tableData: plantMine(action.row, action.cell, action.mine),
halted: false,
};
case OPEN_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
tableData[action.row][action.cell] = CODE.OPENED; // 클릭한 칸을 OPENED으로 바꾸기
return {
...state,
tableData,
};
}
case CLICK_MINE: {
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
tableData[action.row][action.cell] = CODE.CLICKED_MINE;
return {
...state,
tableData,
halted: true,
};
}
case FLAG_CELL:
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.MINE) {
tableData[action.row][action.cell] = CODE.FLAG_MINE;
} else {
tableData[action.row][action.cell] = CODE.FLAG;
}
return {
...state,
tableData,
};
case QUESTION_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.FLAG_MINE) {
tableData[action.row][action.cell] = CODE.QUESTION_MINE;
} else {
tableData[action.row][action.cell] = CODE.QUESTION;
}
return {
...state,
tableData,
};
}
case NORMALIZE_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.FLAG_MINE) {
tableData[action.row][action.cell] = CODE.MINE;
} else {
tableData[action.row][action.cell] = CODE.NORMAL;
}
return {
...state,
tableData,
};
}
default:
return state;
}
};
const reducer = (state, action) => {
switch (action.type) {
case OPEN_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
let around = [];
if (tableData[action.row - 1]) { // 클릭한 칸 기준으로 윗줄이 있으면
around = around.concat(
tableData[action.row - 1][action.cell - 1],
tableData[action.row - 1][action.cell],
tableData[action.row - 1][action.cell + 1]
);
}
around = around.concat(
tableData[action.row][action.cell - 1],
tableData[action.row][action.cell + 1]
);
if (tableData[action.row + 1]) { // 클릭한 칸 기준으로 밑에줄이 있으면
around = around.concat(
tableData[action.row + 1][action.cell - 1],
tableData[action.row + 1][action.cell],
tableData[action.row + 1][action.cell + 1]
);
}
// 주변 지뢰 갯수 찾기
const count = around.filter((v) => [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v)).length;
console.log(around, count);
tableData[action.row][action.cell] = count;
return {
...state,
tableData,
};
}
}
};
case OPEN_CELL: {
const tableData = [...state.tableData];
// tableData[action.row] = [...state.tableData[action.row]];
tableData.forEach((row, i) => { // 클릭한 칸 뿐 아니라 모든 칸을 새로운 객체로 만들기
tableData[i] = [...state.tableData[i]];
});
const checked = [];
const checkAround = (row, cell) => {
// 닫힌 칸이 아니면 리턴
if ([CODE.OPENED, CODE.FLAG_MINE, CODE.FLAG, CODE.QUESTION_MINE, CODE.QUESTION].includes(tableData[row][cell])) {
return;
}
// 상하좌우 없는 칸은 안 열기
if (row < 0 || row >= tableData.length || cell < 0 || cell >= tableData[0].length) {
return;
}
// 옆에칸 서로 검사하는 거 막아주기
if (checked.includes(row + ',' + cell)) { // 이미 검사한 칸이면
return;
} else { // 아니면 checked 배열에 넣어주기
checked.push(row + ',' + cell);
}
let around = [];
if (tableData[row - 1]) {
around = around.concat(
tableData[row - 1][cell - 1],
tableData[row - 1][cell],
tableData[row - 1][cell + 1]
);
}
around = around.concat(
tableData[row][cell - 1],
tableData[row][cell + 1]
);
if (tableData[row + 1]) {
around = around.concat(
tableData[row + 1][cell - 1],
tableData[row + 1][cell],
tableData[row + 1][cell + 1]
);
}
const count = around.filter((v) => [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v)).length;
tableData[row][cell] = count;
// 지뢰가 없으면 주변 8칸 열기
if (count === 0) {
const near = [];
if (row - 1 > -1) {
near.push([row - 1, cell - 1]);
near.push([row - 1, cell]);
near.push([row - 1, cell + 1]);
}
near.push([row, cell - 1]);
near.push([row, cell + 1]);
if (row + 1 < tableData.length) {
near.push([row + 1, cell - 1]);
near.push([row + 1, cell]);
near.push([row + 1, cell + 1]);
}
near.forEach((n) => {
if (tableData[n[0]][n[1]] !== CODE.OPENE) {
checkAround(n[0], n[1]);
}
})
}
};
checkAround(action.row, action.cell);
return {
...state,
tableData,
};
}
승리조건: 클릭한 칸이 가로 * 세로 - 지뢰개수 한 값과 같으면 승리
const initialState = {
tableData: [],
date: {
row: 0,
cell: 0,
mine: 0,
},
result: '',
halted: true,
openedCount: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
data: { // 게임 시작 시 data 기록해두기
row: action.row,
cell: action.cell,
mine: action.mine,
},
openedCount: 0,
tableData: plantMine(action.row, action.cell, action.mine),
halted: false,
timer: 0,
};
case OPEN_CELL: {
let openedCount = 0;
const checkAround = (row, cell) => {
openedCount += 1; // 칸을 열 때마다 1씩 올려주기
...
};
...
};
checkAround(action.row, action.cell);
let halted = false;
let result = '';
console.log(state.data.row * state.data.cell - state.data.mine, state.openedCount, openedCount);
if (state.data.row * state.data.cell - state.data.mine === state.openedCount + openedCount) { // 승리
halted = true; // 승리 시 게임 멈추기
result = '승리하셨습니다!';
}
return {
...
halted,
result
};
}
const initialState = {
...
timer: 0,
};
export const INCREMENT_TIMER = 'INCREMENT_TIMER';
const MineSearch = () => {
useEffect(() => {
let timer;
if (halted === false) { // 게임 시작 시 타이머 실행
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
}
}, [halted]);
};
마지막으로 useMemo 사용하여 최적화 하기