리덕스는 동기적 / useReducer
는 비동기적
Context API를 사용하면 dispatch
를 부모를 모두 거쳐서 전달하지 않고 한번에 전달 가능
createContext
로 기본값 세팅Provider
를 통해 자식 컴포넌트가 데이터에 접근 가능value
로 전달MineSearch.jsx
import react, { useReducer, createContext, useMemo } from 'react';
...
// 초기값 세팅
export const TableContext = createContext({
tableData: [],
dispatch: () => {},
});
...
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => ({tableData: state.tableData, dispatch}), [state.tableData]);
return (
{/* TableContext.Provider에 값을 전달하면 모든 자식 컴포넌트가 해당 값을 전달받을 수 있음*/}
<TableContext.Provider value={value}>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
}
useMemo
를 통해 캐싱을 해서 성능 최적화 필요 (아니면 렌더링될 때마다 새로운 tableData
객체가 생성되기 때문)
dispatch
는 변경되지 않음전달된 값은 useContext
로 접근할 수 있다.
Form.jsx
import react, { useState, useCallback, useContext } from 'react';
...
const Form = () => {
...
// value.dispatch를 구조분해
const { dispatch } = useContext(TableContext);
...
const onClickBtn = useCallback(() => {
dispatch({ type: START_GAME, row, cell, mine });
}, [row, cell, mine]);
return (
<div>
...
<button onClick={onClickBtn}>시작</button>
</div>
);
};
dispatch
로 action START_GAME
전달tableData
에 plantMine
을 실행하여 지뢰 세팅tableData
는 createContext
=> useContext
를 통해 자식 컴포넌트로 전달dispatch
를 한 단계씩 전달하지 않고 데이터 바로 전달 가능Td.jsx
<td
style={getTdStyle(tableData[rowIndex][cellIndex])}
onClick={onClickTd}
onContextMenu={onRightClickTd}
>{getTdText(tableData[rowIndex][cellIndex])}</td>
MineSearch.jsx
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;
tableData[action.row][action.cell] = count;
return {
...state,
tableData,
};
}
undefined
는 filter
함수에 걸려서 사라진다.MineSearch.jsx
case OPEN_CELL: {
const tableData = [...state.tableData];
tableData.forEach((row, i) => {
tableData[i] = [...row];
});
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.push(row + ',' + cell);
}
let around = [tableData[row][cell - 1], tableData[row][cell + 1]]; // 옆칸
if (tableData[row - 1]){ // 윗줄
...
}
if (tableData[row + 1]){ // 아랫줄
...
}
const count = around.filter((v) => [CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v)).length;
if(count === 0) { // 주변에 지뢰가 없으면 연결된 모든 칸 열기
const near = [];
if(row > -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.filter(v => !!v).forEach((n) => {
if(tableData[n[0]][n[1]] !== CODE.OPENED){ // 주변 칸이 닫혀있는 경우
checkAround(n[0], n[1]);
}
});
}
tableData[row][cell] = count;
};
checkAround(action.row, action.cell); // 재귀함수 실행
return {
...state,
tableData,
};
}
(모든 칸 - 지뢰 개수) === 열린 칸
이면 승리useEffect()
로~~memo
, useMemo
를 적절히 사용하여 캐싱을 통해 최적화