Today I Learned ... react.js
🙋♂️ React.js Lecture
🙋 My Dev Blog
React Lecture CH 8
1 - context API
2 - createContext와 Provider
3 - useContext
4 - 좌클릭, 우클릭 로직
5 - 지뢰 개수 표시
6 - 빈칸 한번에 열기
7 - 승리 조건 체크, 타이머
8 - context API 최적화
❕ 참고 - 왜 위,아래만 검색하는건지?
지난번에 OPEN_CELL 로직에서 맨 윗줄인지, 맨 마지막줄인지 체크했었다. 왜 좌,우는 신경쓰지 않는지?
-> 자바스크립트 특성상 tableData[row][cell]에서 row가 없으면 undefined가 나와서 에러가 발생한다.
-> 즉, 에러를 막기 위해서 row를 검사하는 것.
MineSearch.jsx
const checkAround = (row, cell) => {
if (
[
CODE.OPENED,
CODE.FLAG_MINE,
CODE.FLAG,
CODE.MINE,
CODE.QUESTION,
].includes(tableData[row][cell])
) {
return;
}
if (
row < 0 ||
row > tableData.length ||
cell < 0 ||
cell > tableData[0].length
) {
return;
}
// 주변 셀 검사 - 1) row가 맨 위가 아니라면
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]
);
// 2) row가 맨 아래가 아니라면
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;
// 셀 내용에(text) count가 렌더링되게.
tableData[row][cell] = count;
// 🔻 새로 추가된 코드
// 현재 클릭한 게 count가 0이면?
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은 [3,2]와 같이 row, cell이 저장되어있는 배열이므로 이차원배열이다.
near.forEach((n) => {
if (tableData[n[0]][n[1]] !== CODE.OPENED) {
// near에 해당하는 셀들이 OPENED가 아니라면
checkAround(n[0], n[1]);
// 재귀함수. (자신을 다시한번 호출)
}
});
} else {
}
};
const checked = [];
const checkAround = (row, cell) => {
...
// 이미 검사한 셀인지 확인
if (checked.includes(row + ',' + cell)) {
return;
} else {
checked.push(row + ',' + cell);
}
...
}
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);
} // 한 번 연칸은 무시하기
// 만약 없다면 새로 추가하기 (checked에 push)
10 * 10
에서 지뢰가 20개라 치면,10*10
- 20 = 80개의 셀을 무사히 클릭하면 성공임.opendedCount
를 생성.let openedCount = 0;
if (tableData[row][cell] === CODE.NORMAL) {
// 내 칸이 닫힌 칸이면 -> 오픈 카운트 증가
openedCount += 1;
}
const initialState = {
tableData: [],
timer: 0,
result: '',
halted: true,
openedCount: 0, // 👈 추가
};
case OPEN_CELL: {
...
return {
...state,
tableData,
openedCount: state.openedCount + openedCount,
halted,
result,
};
}
-> 맨 처음에 state.openedCount
는 초기값 0이고,
openedCount
는 한번 클릭할때마다 (여러 칸이 열리면 - openCount가 2이상임) +1씩 되어 이전 openedCount
값에 누적된다.
const initialState = {
tableData: [],
data: { // 👈
row: 0,
cell: 0,
mine: 0,
},
timer: 0,
result: '',
halted: true,
openedCount: 0,
};
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,
};
if (
state.data.row * state.data.cell - state.data.mine ===
state.openedCount + openedCount
) {
// 승리시 일시정지 + result
halted = true;
result = `승리하셨습니다`;
}
case INCREMENT_TIMER: {
return {
...state,
timer: state.timer + 1,
};
}
useEffect(() => {
let timer;
if (halted === false) {
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
};
}, [halted]);
-> 1초마다 INCREMENT_TIMER을 dispatch 해줘서 타이머가 1씩 증가하게 함.
❗️ 주의
- if(halted === false) 를 하지 않으면, 시작 버튼을 누르지 않아도 타이머가 증가하게 된다.
- useEffect의 두번째 인자로 halted를 넘겨줘서, halted가 업데이트 될때마다 실행함. (componentDidUpdate)
clearInterval
은 다시 halted가 true가 되었을 때if (
state.data.row * state.data.cell - state.data.mine ===
state.openedCount + openedCount
) {
halted = true;
result = `${state.timer}초만에 승리하셨습니다`; // 👈 추가
}
-> contextAPI는 최적화에 어려움이 있다.
하위 컴포넌트도 memo가 되어있어야 상위 컴포넌트에도 memo가 적용될 수 있다.
✅ 주의 - Form에서 useContext를 사용하기 때문에
어쩔 수 없이 리렌더링이 발생.
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;
-> 컴포넌트를 쪼갤 수 있음.