⑧ 지뢰찾기

hhkim·2020년 2월 9일
0
post-thumbnail

🔗 웹 게임을 만들며 배우는 React

1. Context API 소개와 지뢰찾기

  • 리덕스는 동기적 / useReducer는 비동기적

  • Context API를 사용하면 dispatch를 부모를 모두 거쳐서 전달하지 않고 한번에 전달 가능

2. createContext와 Provider

  • 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>
  );
};

3. useContext 사용해 지뢰 칸 렌더링

  1. '시작' 버튼 클릭
  2. dispatch로 action START_GAME 전달
  3. tableDataplantMine을 실행하여 지뢰 세팅
  4. tableDatacreateContext => useContext를 통해 자식 컴포넌트로 전달
    👉 dispatch를 한 단계씩 전달하지 않고 데이터 바로 전달 가능

4. 왼쪽 오른쪽 클릭 로직 작성하기

Td.jsx

<td
   style={getTdStyle(tableData[rowIndex][cellIndex])}
   onClick={onClickTd}
   onContextMenu={onRightClickTd}
>{getTdText(tableData[rowIndex][cellIndex])}</td>
  • 오른쪽 클릭하면 물음표 => 느낌표(깃발) => 원래 상태 순환

5. 지뢰 개수 표시하기

  • 칸을 클릭했을 때 주변 칸에 대한 배열 생성 => 지뢰 있는 칸 찾아서 개수 세기

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,
   };
 }

6. 빈 칸들 한 번에 열기

  • 재귀함수로 처리
  • undefinedfilter 함수에 걸려서 사라진다.

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,
  };
}

7. 승리 조건 체크와 타이머

  • (모든 칸 - 지뢰 개수) === 열린 칸이면 승리

  • 타이머는 useEffect()로~~

8. Context API 최적화

  • memo, useMemo를 적절히 사용하여 캐싱을 통해 최적화

0개의 댓글