리액트로 구현한
지뢰찾기 게임(ver.1) 입니다.
지뢰찾기 게임(ver.1) 입니다.
- 지뢰판 세팅 -
예제는 인프런의 제로초, "조현영"님의 강의를 들으면서 공부한 내용입니다.
순수 자바스크립트로 만든 지뢰찾기 게임은 아래 포스트로 이동해주세요.
https://goddino.tistory.com/107
구현내용
· useReduer, context API 이용하여 지뢰칸 세팅
· 마우스 오른쪽 클릭하여 로직 작성하기
컴포넌트 구조
MineSearch.jsx >Form.jsx
MineSearch.jsx > MineTable.jsx > MineTr.jsx > MineTd.jsx
Context API
useReducer를 사용할때, 부모로부터 하위간의 레벨이 많을 경우, 계속해서 props로 dispatch를 넘겨줘야 하는 번거로움을 해결하기 위해 context API를 사용합니다.
useReducer에 대한 설명은 아래로 이동해주세요.
https://goddino.tistory.com/153
createContext와 Provider
- createContext() 함수를 사용하여 Context 만들기
- Context API의 데이터에 접근해야 하는 하위 컴포넌트를 return 안에서 Provider로 감싸기
- 전달할 데이터는 value={} 안에 넣기
- context API는 성능 최적화가 힘들기 때문에 useMemo()로 객체값을 기억하여 캐싱 작업 하기
상위 컴포넌트
import React, { useContext } from 'react';
export const TableContext(이름 아무거나) = createContext({ //export하여 하위 컴포넌트에서 import
//초깃값 설정
});
return (
//TableContrect.Provider value에서 data를 넘겨주었기 때문에
//하위 컴포넌트에서 자유롭게 데이터를 쓸 수 있음
<TableContext.Provider value={{ tableData: state.tableData, dispatch }}>
...하위 컴포넌트
</TableContext.Provider>
);
하위 컴포넌트
import React, { useContext } from 'react';
import { TableContext } from "상위 컴포넌트 이름";
const 하위 컴포넌트 = () => {
//const value = useContext(TableContext);
//value.dispatch = useContext(TableContext);
const { dispatch } = useContext(TableContext); //구조분해 dispatch 접근
...
};
화면 결과
client.jsx
import React from 'react';
import ReactDom from 'react-dom';
import MineSearch from './MineSearch';
ReactDom.render(<MineSearch />, document.querySelector('#root'));
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>지뢰찾기</title>
<style>
table {
border-collapse: collapse;
}
td {
border: 2px solid #ddd;
width: 30px;
height: 30px;
text-align: center;
}
input {
max-width: 80px;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="./dist/app.js"></script>
</body>
</html>
MineSearh.jsx
createContext, useReducer, useMemo
import React, { useReducer, createContext, useMemo } from "react";
import Form from "./Form";
import MineTable from "./MineTable";
export const CODE = {
MINE: -7, //지뢰
NORMAL: -1, //정상 칸
QUESTION: -2, //물음표
FLAG: -3, //깃발
QUESTION_MINE: -4, //물음표 자리가 지뢰일 경우
FLAG_MINE: -5, //깃발 자리가 지뢰일 경우
CLICKED_MINE: -6, //지뢰를 클릭했을 경우
OPENED: 0, //open된 칸, 0 이상이면 다 opened
};
export const TableContext = createContext({
//초깃값 설정
tableData: [], //배열모양
halted: true,
dispatch: () => {}, //함수모양
});
const initialState = {
tableData: [],
timer: 0,
result: "",
halted: true, //true일 경우, 게임 stop
};
//지뢰판 만들기
const plantMine = (row, cell, mine) => {
const candidate = Array(cell * row)
.fill()
.map((item, i) => i); //0-99 숫자 중에서
const mixData = [];
while (candidate.length > row * cell - mine) {
//지뢰 갯수 만큼 랜덤으로 추출
const chosen = candidate.splice(
Math.floor(Math.random() * candidate.length),
1
)[0];
mixData.push(chosen);
//console.log("mixData", mixData);
}
//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); //정상 row*cell 칸
}
}
//2차원 배열에 지뢰심기
for (let k = 0; k < mixData.length; k++) {
const ver = Math.floor(mixData[k] / cell);
const hor = mixData[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 NORMAL_CELL = "NORMAL_CELL";
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
tableData: plantMine(action.row, action.cell, action.mine), //plantMine 지뢰심기 함수
halted: false,
};
case OPEN_CELL: {
//클릭하여 칸 열기
//불변성 떄문에 추가 코드
const tableData = [...state.tableData]; //tableData 얕은 복사
tableData[action.row] = [...state.tableData[action.row]];
tableData[action.row][action.cell] = CODE.OPENED; //클릭한 셀이 CODE.OPENED로 바뀜
return {
...state,
tableData,
};
}
case CLICK_MINE: {
const tableData = [...state.tableData]; //tableData 얕은 복사
tableData[action.row] = [...state.tableData[action.row]];
//클릭한 셀이 CODE.OPENED로 바뀜
tableData[action.row][action.cell] = CODE.CLICKED_MINE;
return {
...state,
tableData,
halted: true,
};
}
//flag cell -> question cell -> normal cell
case FLAG_CELL: {
//지뢰 있는 칸 없는 칸 분기처리
const tableData = [...state.tableData]; //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 얕은 복사
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 NORMAL_CELL: {
//지뢰 있는 칸 없는 칸 분기처리
const tableData = [...state.tableData]; //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, initialState);
//value useMemo로 감싸 리랜더링 최적화
//dispatch는 항상 같은 값이므로 deps에 넣지 않음
const { tableData, halted, timer, result } = state;
const value = useMemo(
() => ({ tableData: tableData, halted: halted, dispatch }),
[tableData, halted]
);
return (
//랜더링 할때마다 매번 value 객체가 새롭게 리렌더링 되므로 하위 컴포넌트도 계속
//리렌더링되는 문제 발생 -> useMemo로 캐싱처리
//value = 하위에 전달하는 데이터
//<TableContext.Provider value={{ tableData: state.tableData, dispatch }}>
<TableContext.Provider value={value}>
<Form />
<div>{timer}</div>
<MineTable />
<div>{result}</div>
</TableContext.Provider>
);
};
export default MineSearch;
Form.jsx
context API 설정
import React, { useState, useCallback, useContext } from "react";
import { TableContext } from "./MineSearch";
const Form = () => {
const [row, setRow] = useState(10); //가로-줄
const [cell, setCell] = useState(10); //세로-칸
const [mine, setMine] = useState(20); //지뢰 갯수
//const value = useContext(TableContext);
//value.dispatch = useContext(TableContext);
const { dispatch } = useContext(TableContext); //구조분해 dispatch 접근
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 onClickBtn = useCallback(() => {
//context API 적용
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={onClickBtn}>시작</button>
</div>
);
};
export default Form;
MineTable.jsx
import React, { useContext } from "react";
import { TableContext } from "./MineSearch";
import MineTr from "./MineTr";
const MineTable = () => {
const { tableData } = useContext(TableContext); //value.tabeData를 구조분해
return (
<div>
<table>
<tbody>
{Array(tableData.length)
.fill()
.map((tr, i) => (
<MineTr rowIndex={i} />
))}
</tbody>
</table>
</div>
);
};
export default MineTable;
MineTr.jsx
import React, { useContext } from "react";
import { TableContext } from "./MineSearch";
import MineTd from "./MineTd";
const MineTr = ({ rowIndex }) => {
const { tableData } = useContext(TableContext);
return (
<tr>
{tableData[0] && //tableData[0]이 undefined일 경우 대비
Array(tableData[0].length)
.fill()
.map((td, i) => <MineTd rowIndex={rowIndex} cellIndex={i} />)}
</tr>
);
};
export default MineTr;
MineTd.jsx
지뢰판 화면 렌더링(onClick, onContextMenu 이벤트)
import React, { useCallback, useContext } from "react";
import { CODE, OPEN_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: "#fff",
};
case CODE.FLAG_MINE:
case CODE.FLAG:
return {
background: " pink",
};
case CODE.QUESTION_MINE:
case CODE.QUESTION:
return {
background: "aqua",
};
default:
return {
background: "#fff",
};
}
};
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 "?";
}
};
//1. tableData는 useContext로 받기
//2. 칸, 줄 위치는 상위 컴포넌트로 부터 props로 받음
const MineTd = ({ 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 });
}
}, [tableData[rowIndex][cellIndex], halted]);
const onContextMenuTd = 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; //swich문에 return 또는 break로 필요
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: "NORMAL_CELL", row: rowIndex, cell: cellIndex });
return;
default:
return;
}
},
[tableData[rowIndex][cellIndex], halted]
);
return (
<td
style={getTdStyle(tableData[rowIndex][cellIndex])}
onClick={onClickTd}
onContextMenu={onContextMenuTd}
>
{getTdText(tableData[rowIndex][cellIndex])}
</td>
);
};
export default MineTd;
반응형
'개발 > React' 카테고리의 다른 글
[react] react로 axios로 API 호출 (ft. promise, hooks) (2) | 2021.04.19 |
---|---|
[react] react로 지뢰찾기 게임 만들기 ver.2 (0) | 2021.04.16 |
[react] 리액트 테이블 게시판 만들기 ver.2 (데이터 추가, 수정, 저장, hooks, form) (0) | 2021.04.13 |
[react] 리액트 테이블 게시판 만들기 ver.1 (axios, useEffect, 글삭제) (8) | 2021.04.13 |
[react] react의 useReducer 사용법 (0) | 2021.04.06 |
댓글