#8 Context API / 지뢰찾기
인프런에 올라간 제로초님의 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/web-game-react
코드
함수 버전(Hooks)
Td.jsx
import React, { useContext, useCallback, useMemo, memo } from 'react';
import { CLICK_MINE, CODE, FLAG_CELL, NORMALIZE_CELL, OPEN_CELL, QUESTION_CELL, TableContext } from './MineSearch';
const getTdStyle = (code) => {
switch (code) {
case CODE.NORMAL:
case CODE.MINE:
return {
background: '#444',
};
case CODE.CLICKED_MINE:
case CODE.OPENED:
return {
background: 'white',
};
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return {
background: 'yellow',
};
case CODE.FLAG_MINE:
case CODE.FLAG:
return {
background: 'red',
};
default:
return {
background: 'white',
};
}
};
const getTdText = (code) => {
console.log('getTdtext');
switch (code) {
case CODE.NORMAL:
return '';
case CODE.MINE:
return 'X';
case CODE.CLICKED_MINE:
return '펑';
case CODE.FLAG_MINE:
case CODE.FLAG:
return '!';
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return '?';
default:
return code || '';
}
};
const Td = memo(({ rowIndex, cellIndex }) => {
const { tableData, dispatch, halted } = useContext(TableContext);
const onClickTd = useCallback(() => {
if (halted) {
return;
}
switch (tableData[rowIndex][cellIndex]) {
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]);
console.log('td rendered');
return <RealTd onClickTd={onClickTd} onRightClickTd={onRightClickTd} data={tableData[rowIndex][cellIndex]} />;
});
const RealTd = memo(({ onClickTd, onRightClickTd, data}) => {
console.log('real td rendered');
return (
<td
style={getTdStyle(data)}
onClick={onClickTd}
onContextMenu={onRightClickTd}
>{getTdText(data)}</td>
)
});
export default Td;
Tr.jsx
import React, {memo, useContext} from 'react';
import Td from './Td';
import { TableContext } from './MineSearch';
const Tr = memo(({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;
Table.jsx
import React, {useContext, memo} from 'react';
import Tr from './Tr';
import { TableContext } from './MineSearch';
const Table = memo(() => {
console.log("Table render");
const {tableData} = useContext(TableContext);
console.log(tableData.length);
return (
<table>
{Array(tableData.length).fill().map((tr, i) => <Tr rowIndex={i}/>)}
</table>
);
});
export default Table;
Form.jsx
import React, {useState, useCallback, useContext, memo} from 'react'
import {TableContext, START_GAME} from './MineSearch'
const Form = memo(() => { const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const {dispatch} = useContext(TableContext);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
}, [])
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
}, [])
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
}, [])
const onClickButton = () => {
dispatch({type: START_GAME, row, cell, mine},
[row, cell, mine]);
} //useCallback을 사용하니 row,cell,mine 값을 변경해도 적용이 안됨.
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine}/>
<button onClick={onClickButton}>시작</button>
</div>
)
})
export default Form;
MineSearch.jsx
import React, { useEffect, useReducer, createContext, useMemo } from 'react';
import Table from './Table';
import Form from './Form';
export const CODE = {
MINE: -7,
NORMAL: -1,
QUESTION: -2,
FLAG: -3,
QUESTION_MINE: -4,
FLAG_MINE: -5,
CLICKED_MINE: -6,
OPENED: 0, // 0 이상이면 다 opened
};
export const TableContext = createContext({
tableData: [],
halted: true,
dispatch: () => {},
});
const initialState = {
tableData: [],
data: {
row: 0,
cell: 0,
mine: 0,
},
timer: 0,
result: '',
halted: true,
openedCount: 0,
};
const plantMine = (row, cell, mine) => {
console.log(row, cell, mine);
const candidate = Array(row * cell).fill().map((arr, i) => {
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);
}
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);
}
}
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';
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';
export const INCREMENT_TIMER = 'INCREMENT_TIMER';
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
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: {
const tableData = [...state.tableData];
tableData.forEach((row, i) => {
tableData[i] = [...row];
});
const checked = [];
let openedCount = 0;
console.log(tableData.length, tableData[0].length);
const checkAround = (row, cell) => {
console.log(row, cell);
if (row < 0 || row >= tableData.length || cell < 0 || cell >= tableData[0].length) {
return;
} // 상하좌우 없는칸은 안 열기
if ([CODE.OPENED, CODE.FLAG, CODE.FLAG_MINE, CODE.QUESTION_MINE, CODE.QUESTION].includes(tableData[row][cell])) {
return;
} // 닫힌 칸만 열기
if (checked.includes(row + '/' + cell)) {
return;
} else {
checked.push(row + '/' + cell);
} // 한 번 연칸은 무시하기
let around = [
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]]);
}
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(function (v) {
return [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v);
}).length;
if (count === 0) { // 주변칸 오픈
if (row > -1) {
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.OPENED) {
checkAround(n[0], n[1]);
}
})
}
}
if (tableData[row][cell] === CODE.NORMAL) { // 내 칸이 닫힌 칸이면 카운트 증가
openedCount += 1;
}
tableData[row][cell] = count;
};
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 = `${state.timer}초만에 승리하셨습니다`;
}
return {
...state,
tableData,
openedCount: state.openedCount + openedCount,
halted,
result,
};
}
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.QUESTION_MINE) {
tableData[action.row][action.cell] = CODE.MINE;
} else {
tableData[action.row][action.cell] = CODE.NORMAL;
}
return {
...state,
tableData,
};
}
case INCREMENT_TIMER: {
return {
...state,
timer: state.timer + 1,
}
}
default:
return state;
}
};
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { tableData, halted, timer, result } = state;
const value = useMemo(() => ({ tableData, halted, dispatch }), [tableData, halted]);
useEffect(() => {
let timer;
if (halted === false) {
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
}
}, [halted]);
return (
<TableContext.Provider value={value}>
<Form />
<div>{timer}</div>
<Table />
<div>{result}</div>
</TableContext.Provider>
);
};
export default MineSearch;
#8-1 Context API 개요
//Form.jsx
import React, {useState, useCallback} from 'react'
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
}, [])
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
}, [])
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
}, [])
const onClickButton = useCallback((e) => {
console.log(e);
}, [])
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine}/>
<button onClick={onClickButton}>시작</button>
</div>
)
}
export default Form;
//MineSearch.jsx
import React, {useRef, useState, useReducer, useEffect, useCallback} from 'react';
import Table from './Table'
import Form from './Form'
const initalState = {
tableData : []
};
const reducer = (state, action) => {
}
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initalState);
return (
<>
<Form/>
<div>{state.timer}</div>
<Table/>
<div>{state.result}</div>
</>
)
}
export default MineSearch;
import React, { useContext, useCallback, useMemo, memo } from 'react';
import { CLICK_MINE, CODE, FLAG_CELL, NORMALIZE_CELL, OPEN_CELL, QUESTION_CELL, TableContext } from './MineSearch';
const getTdStyle = (code) => {
switch (code) {
case CODE.NORMAL:
case CODE.MINE:
return {
background: '#444',
};
case CODE.CLICKED_MINE:
case CODE.OPENED:
return {
background: 'white',
};
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return {
background: 'yellow',
};
case CODE.FLAG_MINE:
case CODE.FLAG:
return {
background: 'red',
};
default:
return {
background: 'white',
};
}
};
const getTdText = (code) => {
console.log('getTdtext');
switch (code) {
case CODE.NORMAL:
return '';
case CODE.MINE:
return 'X';
case CODE.CLICKED_MINE:
return '펑';
case CODE.FLAG_MINE:
case CODE.FLAG:
return '!';
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return '?';
default:
return code || '';
}
};
const Td = memo(({ rowIndex, cellIndex }) => {
const { tableData, dispatch, halted } = useContext(TableContext);
const onClickTd = useCallback(() => {
if (halted) {
return;
}
switch (tableData[rowIndex][cellIndex]) {
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]);
console.log('td rendered');
return <RealTd onClickTd={onClickTd} onRightClickTd={onRightClickTd} data={tableData[rowIndex][cellIndex]} />;
});
const RealTd = memo(({ onClickTd, onRightClickTd, data}) => {
console.log('real td rendered');
return (
<td
style={getTdStyle(data)}
onClick={onClickTd}
onContextMenu={onRightClickTd}
>{getTdText(data)}</td>
)
});
export default Td;
import React, {memo, useContext} from 'react';
import Td from './Td';
import { TableContext } from './MineSearch';
const Tr = memo(({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, memo} from 'react';
import Tr from './Tr';
import { TableContext } from './MineSearch';
const Table = memo(() => {
console.log("Table render");
const {tableData} = useContext(TableContext);
console.log(tableData.length);
return (
<table>
{Array(tableData.length).fill().map((tr, i) => <Tr rowIndex={i}/>)}
</table>
);
});
export default Table;
import React, {useState, useCallback, useContext, memo} from 'react'
import {TableContext, START_GAME} from './MineSearch'
const Form = memo(() => { const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const {dispatch} = useContext(TableContext);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
}, [])
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
}, [])
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
}, [])
const onClickButton = () => {
dispatch({type: START_GAME, row, cell, mine},
[row, cell, mine]);
} //useCallback을 사용하니 row,cell,mine 값을 변경해도 적용이 안됨.
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine}/>
<button onClick={onClickButton}>시작</button>
</div>
)
})
export default Form;
import React, { useEffect, useReducer, createContext, useMemo } from 'react';
import Table from './Table';
import Form from './Form';
export const CODE = {
MINE: -7,
NORMAL: -1,
QUESTION: -2,
FLAG: -3,
QUESTION_MINE: -4,
FLAG_MINE: -5,
CLICKED_MINE: -6,
OPENED: 0, // 0 이상이면 다 opened
};
export const TableContext = createContext({
tableData: [],
halted: true,
dispatch: () => {},
});
const initialState = {
tableData: [],
data: {
row: 0,
cell: 0,
mine: 0,
},
timer: 0,
result: '',
halted: true,
openedCount: 0,
};
const plantMine = (row, cell, mine) => {
console.log(row, cell, mine);
const candidate = Array(row * cell).fill().map((arr, i) => {
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);
}
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);
}
}
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';
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';
export const INCREMENT_TIMER = 'INCREMENT_TIMER';
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
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: {
const tableData = [...state.tableData];
tableData.forEach((row, i) => {
tableData[i] = [...row];
});
const checked = [];
let openedCount = 0;
console.log(tableData.length, tableData[0].length);
const checkAround = (row, cell) => {
console.log(row, cell);
if (row < 0 || row >= tableData.length || cell < 0 || cell >= tableData[0].length) {
return;
} // 상하좌우 없는칸은 안 열기
if ([CODE.OPENED, CODE.FLAG, CODE.FLAG_MINE, CODE.QUESTION_MINE, CODE.QUESTION].includes(tableData[row][cell])) {
return;
} // 닫힌 칸만 열기
if (checked.includes(row + '/' + cell)) {
return;
} else {
checked.push(row + '/' + cell);
} // 한 번 연칸은 무시하기
let around = [
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]]);
}
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(function (v) {
return [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v);
}).length;
if (count === 0) { // 주변칸 오픈
if (row > -1) {
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.OPENED) {
checkAround(n[0], n[1]);
}
})
}
}
if (tableData[row][cell] === CODE.NORMAL) { // 내 칸이 닫힌 칸이면 카운트 증가
openedCount += 1;
}
tableData[row][cell] = count;
};
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 = `${state.timer}초만에 승리하셨습니다`;
}
return {
...state,
tableData,
openedCount: state.openedCount + openedCount,
halted,
result,
};
}
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.QUESTION_MINE) {
tableData[action.row][action.cell] = CODE.MINE;
} else {
tableData[action.row][action.cell] = CODE.NORMAL;
}
return {
...state,
tableData,
};
}
case INCREMENT_TIMER: {
return {
...state,
timer: state.timer + 1,
}
}
default:
return state;
}
};
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { tableData, halted, timer, result } = state;
const value = useMemo(() => ({ tableData, halted, dispatch }), [tableData, halted]);
useEffect(() => {
let timer;
if (halted === false) {
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
}
}, [halted]);
return (
<TableContext.Provider value={value}>
<Form />
<div>{timer}</div>
<Table />
<div>{result}</div>
</TableContext.Provider>
);
};
export default MineSearch;
//Form.jsx
import React, {useState, useCallback} from 'react'
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
}, [])
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
}, [])
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
}, [])
const onClickButton = useCallback((e) => {
console.log(e);
}, [])
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine}/>
<button onClick={onClickButton}>시작</button>
</div>
)
}
export default Form;
//MineSearch.jsx
import React, {useRef, useState, useReducer, useEffect, useCallback} from 'react';
import Table from './Table'
import Form from './Form'
const initalState = {
tableData : []
};
const reducer = (state, action) => {
}
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initalState);
return (
<>
<Form/>
<div>{state.timer}</div>
<Table/>
<div>{state.result}</div>
</>
)
}
export default MineSearch;
지뢰찾기 → Form, Table → Tr → Td 형태
state를 바꾸고자 할 때 props로 받은 dipatch로 부모 컴포넌트에 액션을 보내는 방식 대신 Context API로 직통으로 보내는 방식을 사용할 것.
#8-2 createContext와 Provider
createContext
const 객체 = createContext({})
//Form.jsx
import React, {useState, useCallback, useContext} from 'react'
import {TableContext, START_GAME} from './MineSearch'
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const {dispatch} = useContext(TableContext);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
}, [])
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
}, [])
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
}, [])
const onClickButton = useCallback((e) => {
dispatch({type: START_GAME, row, cell, mine},
[row, cell, mine])
}, [])
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine}/>
<button onClick={onClickButton}>시작</button>
</div>
)
}
export default Form;
//MineSearch.jsx
const TableContext = createContext({
tableData : [],
dispatch : () => {},
})
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initalState);
return (
<>
<TableContext.Provider value={{tableData : state.tableData, dispatch}}>
<Form/>
<div>{state.timer}</div>
<Table/>
<div>{state.result}</div>
</TableContext.Provider>
</>
)
}
export default MineSearch;
Context API에서도 state와 비슷한 데이터 영역이 존재한다.
Provider로 감싸여있는 컴포넌트라면 리액트에서 useContext를, 부모 컴포넌트에서 createContext로 생성된 객체(TableContext)를 inport해서 useContext(객체) 형태로 사용할 수 있다.
Provider
컴포넌트들을 <createContext를 대입받은 객체.Provider>
태그로 감싸줌으로써 createContext 함수 내부의 데이터에 접근할 수 있게 해준다.
#8-3 지뢰 칸 렌더링
import React, {useState, useCallback, useContext} from 'react'
import {TableContext, START_GAME} from './MineSearch'
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const {dispatch} = useContext(TableContext);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
}, [])
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
}, [])
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
}, [])
const onClickButton = useCallback(() => {
dispatch({type: START_GAME, row, cell, mine},
[row, cell, mine]);
}, []) //useCallback을 사용하니 row,cell,mine 값을 변경해도 적용이 안됨.
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine}/>
<button onClick={onClickButton}>시작</button>
</div>
)
}
export default Form;
MineSearch에서부터 Td로 가면서 해당 칸의 인덱스에 대한 정보를 props로 넘겨줌.
Td에서 dispatch를 보낼 때는 TableContext의 dispatch를 import해서 한 번에 보낼 수 있다.
props를 받을 때 객체 형식({index : 1})으로 넘어오기 때문에 값만을 받고 싶다면 구조 분해 할당으로 받아야 한다.
#8-4 좌클릭 우클릭 구현
///Td.jsx
import React, {useCallback, memo, useContext} from 'react';
import { CLICK_MINE, CODE, FLAG_CELL, NORMALIZE_CELL, OPEN_CELL, QUESTION_CELL, TableContext } from './MineSearch';
const getTdStyle = (code) => {
switch (code) {
case CODE.NORMAL:
case CODE.MINE:
return {
background: '#444',
};
case CODE.CLICKED_MINE:
case CODE.OPENED:
return {
background: 'white',
};
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return {
background: 'yellow',
};
case CODE.FLAG_MINE:
case CODE.FLAG:
return {
background: 'red',
};
default:
return {
background: 'white',
};
}
}
const getTdText = (code) => {
switch (code) {
case CODE.NORMAL:
return '';
case CODE.MINE:
return 'X';
case CODE.CLICKED_MINE:
return '펑';
case CODE.FLAG_MINE:
case CODE.FLAG:
return '!';
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return '?';
default:
return code || '';
}
};
const Td = memo(({rowIndex, cellIndex}) => {
const {tableData, dispatch, halted} = useContext(TableContext);
//console.log("Td render");
const onClickTd = useCallback(() => {
if (halted) {
return ;
}
switch (tableData[rowIndex][cellIndex]) {
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 onClick={onClickTd} onContextMenu={onRightClickTd}
rowIndex={rowIndex} cellIndex={cellIndex} style={getTdStyle(tableData[rowIndex][cellIndex])}>
{getTdText(tableData[rowIndex][cellIndex])}
</td>
)
});
export default Td;
///MineSearch.jsx
import React, {useRef, useState, useReducer, useEffect, useCallback, createContext, useMemo} from 'react';
import Table from './Table'
import Form from './Form'
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';
export const INCREMENT_TIMER = 'INCREMENT_TIMER'
export const CODE = {
OPENED : 0,
NORMAL: -1,
QUESTION : -2,
FLAG : -3,
QUESTION_MINE : -4,
FLAG_MINE : -5,
CLICKED_MINE : -6,
MINE: -7,
}
export const TableContext = createContext({ //없어도 무방?
tableData : [],
dispatch : () => {},
halted : false,
})
const initalState = {
tableData : [],
timer : 0,
result : "",
halted : false,
};
const plantMine = (row, cell, mine) => {
const candidate = Array(row * cell).fill().map((arr, i) => {
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);
}
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);
}
}
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hot = shuffle[k] % cell;
data[ver][hot] = CODE.MINE;
}
console.log(data);
return (data);
}
const reducer = (state, action) => {
switch(action.type){
case START_GAME : {
console.log(action)
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;
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.QUESTION_MINE) {
tableData[action.row][action.cell] = CODE.MINE
} else {
tableData[action.row][action.cell] = CODE.NORMAL
}
return {
...state,
tableData,
}
}
default: return state;
}
}
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initalState);
const { tableData, halted, timer, result } = state;
const value = useMemo(() => ({ tableData, halted, dispatch }), [tableData, halted]);
return (
<>
<TableContext.Provider value={value}>
<Form/>
<div>{timer}</div>
<Table/>
<div>{result}</div>
</TableContext.Provider>
</>
)
}
export default MineSearch;
태그에 onClick 같은 이벤트를 등록했을 때 따로 인자를 주지 않아도 해당 태그의 속성 값은 전달된다.
cell의 상태에 따라 다른 처리.
우클릭으로 뒤집혀지지 않은 셀의 상태 지정. 밑에 지뢰가 있냐 없냐 조건문을 걸어 각기 다르게 저장.
onContextMenu
우클릭 감지하기 위한 HTML 속성
e.preventDefault()를 써주지 않으면 일반적으로 우클릭 시 나오는 팝업이 등장한다.
#8-5 지뢰 개수 표시
//MineSearch.jsx
import React, {useRef, useState, useReducer, useEffect, useCallback, createContext, useMemo} from 'react';
import Table from './Table'
import Form from './Form'
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';
export const INCREMENT_TIMER = 'INCREMENT_TIMER'
export const CODE = {
OPENED : 0,
NORMAL: -1,
QUESTION : -2,
FLAG : -3,
QUESTION_MINE : -4,
FLAG_MINE : -5,
CLICKED_MINE : -6,
MINE: -7,
}
export const TableContext = createContext({ //없어도 무방?
tableData : [],
dispatch : () => {},
halted : false,
})
const initalState = {
tableData : [],
timer : 0,
result : "",
halted : false,
};
const plantMine = (row, cell, mine) => {
const candidate = Array(row * cell).fill().map((arr, i) => {
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);
}
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);
}
}
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hot = shuffle[k] % cell;
data[ver][hot] = CODE.MINE;
}
console.log(data);
return (data);
}
const reducer = (state, action) => {
switch(action.type){
case START_GAME : {
console.log(action)
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]];
const checkAround = (row, cell) => {
console.log(row, cell);
let around = [
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]]);
}
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) => {
return [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v);
}).length;
return count;
}
tableData[action.row][action.cell] = checkAround(action.row, action.cell);
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.QUESTION_MINE) {
tableData[action.row][action.cell] = CODE.MINE
} else {
tableData[action.row][action.cell] = CODE.NORMAL
}
return {
...state,
tableData,
}
}
default: return state;
}
}
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initalState);
const { tableData, halted, timer, result } = state;
const value = useMemo(() => ({ tableData, halted, dispatch }), [tableData, halted]);
return (
<>
<TableContext.Provider value={value}>
<Form/>
<div>{timer}</div>
<Table/>
<div>{result}</div>
</TableContext.Provider>
</>
)
}
export default MineSearch;
주변에 있는 지뢰 개수가 나타도록 하기
가장자리의 경우 따로 예외처리 해주기
concat
배열.concat(다른 배열)
배열 뒤에 붙여서 사용한다. 두 개의 배열을 합쳐서 하나의 배열로 리턴하는 함수.
#8-6 재귀로 빈칸 열기
const reducer = (state, action) => {
switch(action.type){
case START_GAME : {
console.log(action)
return {
...state,
tableData : plantMine(action.row, action.cell, action.mine),
halted : false,
}
}
case OPEN_CELL: {
const tableData = [...state.tableData];
tableData.forEach((row, i) => {
tableData[i] = [...row];
});
const checked = [];
let openedCount = 0;
console.log(tableData.length, tableData[0].length);
const checkAround = (row, cell) => {
console.log(row, cell);
if (row < 0 || row >= tableData.length || cell < 0 || cell >= tableData[0].length) {
return;
} // 상하좌우 없는칸은 안 열기
if ([CODE.OPENED, CODE.FLAG, CODE.FLAG_MINE, CODE.QUESTION_MINE, CODE.QUESTION].includes(tableData[row][cell])) {
return;
} // 닫힌 칸만 열기
if (checked.includes(row + '/' + cell)) {
return;
} else {
checked.push(row + '/' + cell);
} // 한 번 연칸은 무시하기
let around = [
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]]);
}
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(function (v) {
return [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v);
}).length;
if (count === 0) { // 주변칸 오픈
if (row > -1) {
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.OPENED) {
checkAround(n[0], n[1]);
}
})
}
}
if (tableData[row][cell] === CODE.NORMAL) { // 내 칸이 닫힌 칸이면 카운트 증가
openedCount += 1;
}
tableData[row][cell] = count;
};
checkAround(action.row, action.cell);
return {
...state,
tableData,
}
}
}
빈칸을 열면 그 주변의 8칸을 대상으로 재귀함수를 반복. 재귀가 끝나는 조건은 주변이 칸을 벗어나거나 빈칸이 아니거나 이미 열었거나로 지정.
한 번 연 칸은 다시 검사하지 않도록 해서 무한 루프에 빠지는 것을 방지(콜스택이 터지는 것을 방지)
#8-7
import React, { useEffect, useReducer, createContext, useMemo } from 'react';
import Table from './Table';
import Form from './Form';
export const CODE = {
MINE: -7,
NORMAL: -1,
QUESTION: -2,
FLAG: -3,
QUESTION_MINE: -4,
FLAG_MINE: -5,
CLICKED_MINE: -6,
OPENED: 0, // 0 이상이면 다 opened
};
export const TableContext = createContext({
tableData: [],
halted: true,
dispatch: () => {},
});
const initialState = {
tableData: [],
data: {
row: 0,
cell: 0,
mine: 0,
},
timer: 0,
result: '',
halted: true,
openedCount: 0,
};
const plantMine = (row, cell, mine) => {
console.log(row, cell, mine);
const candidate = Array(row * cell).fill().map((arr, i) => {
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);
}
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);
}
}
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';
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';
export const INCREMENT_TIMER = 'INCREMENT_TIMER';
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
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: {
const tableData = [...state.tableData];
tableData.forEach((row, i) => {
tableData[i] = [...row];
});
const checked = [];
let openedCount = 0;
console.log(tableData.length, tableData[0].length);
const checkAround = (row, cell) => {
console.log(row, cell);
if (row < 0 || row >= tableData.length || cell < 0 || cell >= tableData[0].length) {
return;
} // 상하좌우 없는칸은 안 열기
if ([CODE.OPENED, CODE.FLAG, CODE.FLAG_MINE, CODE.QUESTION_MINE, CODE.QUESTION].includes(tableData[row][cell])) {
return;
} // 닫힌 칸만 열기
if (checked.includes(row + '/' + cell)) {
return;
} else {
checked.push(row + '/' + cell);
} // 한 번 연칸은 무시하기
let around = [
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]]);
}
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(function (v) {
return [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v);
}).length;
if (count === 0) { // 주변칸 오픈
if (row > -1) {
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.OPENED) {
checkAround(n[0], n[1]);
}
})
}
}
if (tableData[row][cell] === CODE.NORMAL) { // 내 칸이 닫힌 칸이면 카운트 증가
openedCount += 1;
}
tableData[row][cell] = count;
};
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 = `${state.timer}초만에 승리하셨습니다`;
}
return {
...state,
tableData,
openedCount: state.openedCount + openedCount,
halted,
result,
};
}
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.QUESTION_MINE) {
tableData[action.row][action.cell] = CODE.MINE;
} else {
tableData[action.row][action.cell] = CODE.NORMAL;
}
return {
...state,
tableData,
};
}
case INCREMENT_TIMER: {
return {
...state,
timer: state.timer + 1,
}
}
default:
return state;
}
};
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { tableData, halted, timer, result } = state;
const value = useMemo(() => ({ tableData, halted, dispatch }), [tableData, halted]);
useEffect(() => {
let timer;
if (halted === false) {
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
}
}, [halted]);
return (
<TableContext.Provider value={value}>
<Form />
<div>{timer}</div>
<Table />
<div>{result}</div>
</TableContext.Provider>
);
};
export default MineSearch;
게임 시작시 맵의 정보를 state에 저장. 맵의 최대 크기에서 지뢰 개수를 뺀 것만큼 타일을 열었다면 승리하도록 조건을 검.
게임을 재시작하면 몇 개를 열었는지 세고 있는 것도 다시 초기화.
게임 시작 버튼을 누를 때 시간을 세도록 하고 게임이 끝나면 멈춰서 얼마나 지났는지를 출력.
#8-8 최적화
Context API를 사용할 때, 즉 TableCeontext.Provider 태그로 컴포넌트를 감싸며 props를 전달할 때 memo로 사전에 감싸줘야 한다.
Context API를 사용하면 불가피하게 리렌더링 되는 부분이 있다.
함수 자체는 여러번 호출되어도 리렌더링은 한 번만 되게 설정해주어야 한다.
Author And Source
이 문제에 관하여(#8 Context API / 지뢰찾기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sham/8-Context-API-지뢰찾기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)