22-04-20 (3)

thisisyjin·2022년 4월 20일
0

TIL 📝

목록 보기
27/113

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


지난 시간 리뷰

  • React.createContext
    export하여 하위 컴포넌트에서도 사용 가능하게.
    -> ()안에 초기값을 작성해준다.

  • useContext
    import한 context명을 작성해준다.

  • JSX내에서 <Provider value={value}>
    -> value는 useMemo로 캐싱해줘야 성능 최적화 가능.

React Lecture CH 8

1 - context API
2 - createContext와 Provider
3 - useContext

4 - 좌클릭, 우클릭 로직
5 - 지뢰 개수 표시
6 - 빈칸 한번에 열기
7 - 승리 조건 체크, 타이머
8 - context API 최적화

좌클릭, 우클릭 로직

1) 좌클릭 - OPEN_CELL

MineSearch.jsx

const reducer = (state, action) => {
  switch (action.type) {
    case START_GAME:
      return {
        ...state,
        tableData: plantMine(action.row, action.cell, action.mine),
      };
      // 🔻 추가
    case OPEN_CELL: {
      const tableData = [...state.tableData];
      tableData[action.row] = [...state.tableData[action.row]];
      tableData[action.row][action.cell] = CODE.OPENED;
      return {
        ...state,
        tableData,
      };
    }

    default:
      return state;
  }
};
  • 얕은 복사를 한 후([...]), 해당 셀에 CODE.OPENED를 할당하면 된다.

Td.jsx

const Td = ({ rowIndex, cellIndex }) => {
  const { tableData, dispatch } = useContext(TableContext);

  const onClickTd = useCallback(() => {
    dispatch({ type: OPEN_CELL, row: rowIndex, cell: cellIndex });
  }, []);

  return (
    <td style={getTdStyle(tableData[rowIndex][cellIndex])} onClick={onClickTd}>
      {getTdText(tableData[rowIndex][cellIndex])}
    </td>
  );
};
  • Td를 클릭했을때 발생하는 onClickTd 함수를 작성함.
  • JSX 어트리뷰트에 직접 넣는 함수는 -> useCallback 사용!
  • dispatch를 useContext로 받아온 후, action 객체를 넣어줌.
    -> row, cell 정보는 각각 (props로 받은) rowIndex, cellIndex를 대입.

<결과>
Td를 클릭시 하얀색으로 변함.
즉, tableData[rowIndex][cellIndex]에 CODE.OPENED가 대입되어, getTdStyle에 의해 background가 #fff가 되는 것.

해결할 점

  • 지뢰가 있는 부분도 하얗게 됨
  • 숫자가 나와야 함

onClickTd 세분화

  • tableData[rowIndex][cellIndex] 의 값에 따라서 동작이 달라져야함.
    -> switch문을 이용.

Td.jsx

const onClickTd = useCallback(() => {
    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;
    }
  }, [tableData[rowIndex][cellIndex]]);
  • OPENED, FLAG, QUESTION일때는 클릭해도 변화가 없게 함. (return;으로 빠져나감)
  • NORMAL 일때는 OPEN_CELL 액션이 실행되도록.
    (상태는 OPENED가 되고, 스타일도 적용)
  • MINE을 클릭했을때는 CLICK_MINE 액션이 실행되도록. (지뢰가 터지는 액션. 추후 작성함)
  • useCallback 두번째 인자는 그 값이 바뀔때마다 함수도 다시 저장되도록 함.

스타일 + 텍스트 세분화

Td 컴포넌트의 getTdStyle, getTdText를 수정해보자.

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.QUESTION:
    case CODE.QUESTION_MINE:
      return {
        background: 'yellow', 
      };
    case CODE.FLAG:
    case CODE.FLAG_MINE:
      return {
        background: 'red',
      };
    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:
    case CODE.FLAG_MINE:
      return '!';
    case CODE.QUESTION:
    case CODE.QUESTION_MINE:
      return '?';
    default:
      return '';
  }
};

2) 우클릭 - onContextMenu

Td.jsx

const onRightClickTd = useCallback(
    (e) => {
      e.preventDefault();
      switch (tableData[rowIndex][cellIndex]) {
        case CODE.NORMAL:
        case CODE.MINE:
          dispatch({ type: FLAG_CELL, row: rowIndex, cell: cellIndex });
          return;
        case CODE.FLAG:
        case CODE.FLAG_MINE:
          dispatch({ type: QUESTION_CELL, row: rowIndex, cell: cellIndex });
          return;
        case CODE.QUESTION:
        case CODE.QUESTION_MINE:
          dispatch({ type: NORMALIZE_CELL, row: rowIndex, cell: cellIndex });
          return;

        default:
          return;
      }
    },
    [tableData[rowIndex][cellIndex]]
  );

// JSX
return (
    <td
      style={getTdStyle(tableData[rowIndex][cellIndex])}
      onClick={onClickTd}
      onContextMenu={onRightClickTd}
    >
      {getTdText(tableData[rowIndex][cellIndex])}
    </td>
  );
  • 우클릭시 기본 동작을 막기위해 (컨텍스트메뉴가 뜨는 것) e.preventDefault를 해준다.
  • onRightClickTd 함수도 tableData[rowIndex][cellIndex]의 값에 의해 동작이 달라져야 하므로, switch문 작성.

MineSearch.jsx

...

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로 모듈로 만들고, Td 에서 전부 import함.

추가 state - halted

  • 지뢰가 터졌을때 게임을 일시정지하는 state.
const initialState = {
  tableData: [],
  timer: 0,
  result: '',
  halted: false, // 👈 추가
};
  • 이제, CLICK_CELL 액션을 처리하는 reducer을 작성해보면
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,
      };
    }
  • halted를 true로 하여 게임이 중단되게 한다.

  • 단, START_GAME 액션에 return 부분에 halted: false를 추가하여 게임을 다시 시작할 수 있도록.

case START_GAME:
      return {
        ...state,
        tableData: plantMine(action.row, action.cell, action.mine),
        halted: false,
      };

halted를 initialState와 creactContext에도 추가해주고,
Td 컴포넌트에서 useContext()로 불러와준다.

MineSearch.jsx

export const TableContext = createContext({
  tableData: [],
  halted: true,
  dispatch: () => {},
});

const initialState = {
  tableData: [],
  timer: 0,
  result: '',
  halted: true,
};
  • 초기값은 true로 하고, 처음에 버튼 클릭시 false가 되게 한다.

Td.jsx

  const { tableData, dispatch, halted } = useContext(TableContext);
  1. 초기값은 true
  2. 시작 버튼 클릭시 false가 됨
  3. if(halted) return; 으로 필터링 함.
    -> onClickTd, onRightClickTd에서.
    -> halter가 true면 셀 클릭 못하게 (게임 중단)
const onClickTd = useCallback(() => {
    if (halted) {
      return;
    }
  ...
}
  
const onRightClickTd = useCallback(
    (e) => {
      e.preventDefault();
      if (halted) {
        return;
      }
   ... 
}
      

나머지 reducer 작성

  • FLAG_CELL, QUESTION_CELL, NORMALIZE_CELL에 대한 reducer도 작성해줌.

MineSearch.jsx

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,
      };
    }
  • 전부 원리는 같다.
  • 우선, 원본이 변하면 안되므로, 얕은 복사를 통해 tableData를 복사한다.
  • tableData[action.row]도 얕은 복사 한다.
    (이차원 배열이므로 펼쳐줘야함)
  • if-else 로직으로 일반 셀인지 (NORMAL), 지뢰 셀인지 (MINE) 체크해서 구분해줌.

Result


지뢰 갯수 나타내기

  • 빈 칸을 누르면 주변에 지뢰가 몇 개가 있는지 나타내야 함
    -> OPEN_CELL의 reducer에 작성.

  • 빈칸만 여러개 있는 곳을 눌렀을 때는, 주변 칸까지 한번에 열리게
    -> 재귀를 통해 구현.

주변 지뢰 개수 카운트

  • OPEN_CELL 로직 구현.
  • 위 or 아래칸이 존재하는가에 대한 차이로
    검사해야할 주변 셀의 개수가 달라짐.
    -> 만약

MineSearch.jsx

case OPEN_CELL: {
      const tableData = [...state.tableData];
      tableData[action.row] = [...state.tableData[action.row]];
      tableData[action.row][action.cell] = CODE.OPENED;

      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;
      console.log(around, count);
      tableData[action.row][action.cell] = count;

      return {
        ...state,
        tableData,
      };
    }
  • concat은 배열과 배열을 합쳐줌.
    (원본 변경은 x. 새 배열 반환)
  • 주변에 지뢰가 있는지 카운트하여 개수를 셈.
    -> filter([].includes()) 를 해서 지뢰 셀을 저장하고, 이 배열의 length는 곧 지뢰의 개수임. Td.jsx
    const getTdText = (code) => {
     switch (code) {
         		...
             default:
         return code || '';
     }
  • 기본적으로 code (=tableData[rowIndex][cellIndex])를 출력하고,
    0이면(지뢰가없으면) 공백을 출력하게.

다음시간 - 빈칸 한번에 열리게 (재귀함수)

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글