TIL 22-04-20 (2)

61702 단어 ReactReact

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


지난 시간 리뷰

  • useReducer

state를 하나로 관리하기 위함.
action객체를 실행하는 dispatch.
reducer로 action을 처리함.

  • useReducer과 Redux의 차이

Redux는 state를 동기적으로 변경되지만,
useReducer은 비동기적으로 변경됨. (useEffect 필요)

  • BUT. props를 자손 컴포넌트에게 물려줄때 모든 중간 컴포넌트들을 다 거쳐야 했음.
    (예- tableData)
    -> 이를 해결하기 위해서는 context API를 사용.

React Lecture CH 8

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

지뢰 찾기 초기코드

  • 컴포넌트 구성
  • useReducer을 사용하되, 이번에는 context API를 추가로 이용함.

MineSearch.jsx

import React, { useReducer } from 'react';
import Table from './Table';
import Form from './Form';

const initialState = {
  tableData: [],
  timer: 0,
  result: '',
};

const reducer = (state, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

const MineSearch = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <Form />
      <div>{state.timer}</div>
      <Table />
      <div>{state.result}</div>
    </>
  );
};

export default MineSearch;
  • 하위 컴포넌트인 Form은 가로*세로의 크기와 지뢰 개수를 정하는 Input들과 '시작'버튼이 존재함.

  • Form 컴포넌트는 하위 컴포넌트이고, useReducer이 아닌 useState를 이용해서 작성.

Form.jsx

import React, { useState, useCallback } from 'react';

const Form = () => {
  const [row, setRow] = useState(10);
  const [cell, setCell] = useState(10);
  const [mine, setMine] = useState(20);

  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(() => {});

  return (
    <>
      <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>
    </>
  );
};

export default Form;

Form 컴포넌트의 button을 클릭시 (onClickBtn)
게임이 시작되도록 해야 함.

-> MineSearch 컴포넌트에서 dispatch를 넘겨줘야함.

  • 기존 방법 - props로 넘겨준다.
return <Form dispatch={dispatch}/>
  • 새로운 방법 - context API 이용
    -> 그 아래에 있는 모든 컴포넌트가 다 받을 수 있음.
    (중간 컴포넌트들을 불필요하게 거칠 필요 X)

createContext와 Provider

createContext

import React, { useReducer, createContext } from 'react';

const TableContext = createContext(); // 기본값 넣을수 있음

Provider

  • 데이터에 접근하고 싶은 컴포넌트를 context API의 Provider로 묶어줘야함.
  • 전달해줄 데이터는 value 어트리뷰트로 적어줌.
return (
    <TableContext.Provider
      value={{tableData: state.tableData, dispatch}}
    >
      <Form />
      <div>{state.timer}</div>
      <Table />
      <div>{state.result}</div>
    </TableContext.Provider>
  );

Provider 란?

  • 관찰자 패턴 (데이터는 공급자가 관리하고, 관찰자는 공급자를 구독하여 데이터를 얻는 방식.)

    -> 지난 포스팅 참조

초기값 설정 + export 해주기

export const TableContext = createContext({
  // 초기값. 여기선 중요하지 않으니 모양만 맞춰주기
    tableData: [],
    dispatch: () => {}
}); 

-> 다른 컴포넌트에서 import 하여 사용 가능하도록 export 해줌.

useContext

Form.jsx

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);
  
  ...
}
  1. useContext도 임포트해줌
  2. MineSearch 컴포넌트의 TableContext를 임포트해옴.
  3. const value = useContext(TableContext) 해줌.
    -> dispatch를 사용하기 위해서 value.dispatch 와 같이 사용 가능.
    -> 가독성을 위해 { dispatch } 와 같이 구조분해 할당 권장.

✅ 주의 - 성능 최적화 문제

  • context API 사용시 성능 최적화가 매우 어려움.
return (
    <TableContext.Provider value={{ tableData: state.tableData, dispatch }}>
      <Form />
      <div>{state.timer}</div>
      <Table />
      <div>{state.result}</div>
    </TableContext.Provider>
  );

-> 여기서 컴포넌트가 렌더링 될 때 마다
value={{ tableData: state.tableData, dispatch }} 객체가 계속 생성되므로 자식들도 매번 새롭게 리렌더링 됨.

  • useMemo를 통한 캐싱(caching) 이 필요하다.
const MineSearch = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const value = useMemo(
    () => ({
      tableData: state.tableData,
      dispatch,
    }),
    [state.tableData]
  );
  
  return (
    <TableContext.Provider value={value}>
      <Form />
      <div>{state.timer}</div>
      <Table />
      <div>{state.result}</div>
    </TableContext.Provider>
  );
};
  • useMemo는 함수의 값을 저장한다.
  • tableData가 바뀔 때마다 값을 다시 저장하도록 한다.

dispatch

Form.jsx

const onClickBtn = useCallback(() => {
      dispatch({type: START_GAME, row, cell, mine })
  }, [row, cell, mine]);
  • action을 적어주고 dispatch로 실행해줌.
    -> Form 컴포넌트에서 사용자가 설정한 row, cell, mine을 전달해줘야 함.

MineSearch.jsx

  • action을 등록해주고, reducer에도 case문으로 작성.
...

export const START_GAME = 'START_GAME';

const reducer = (state, action) => {
  switch (action.type) {
    case START_GAME: {
      return {
        ...state,
        tableData: plantMine(action.row, action.cell, action.mine),
      };
    }
    default:
      return state;
  }
};
  • 게임이 시작되면
    -> 1) row*cell 테이블을 만들고, 2) 지뢰 개수만큼 심어야 함.
    -> 코드의 양이 많으므로 하나의 함수로 빼줌. (plantMine 함수)

지뢰찾기 로직

tableData를 변경하는데, 지난 틱택토 예제와 마찬가지로 2차원 배열로 구성된다.

지뢰는 -7로 표시하고, 일반 셀은 -1로, 그 외에도 다양한 상태들을 숫자로 표기하여 구분한다.

예>

[-1, -1, -1, -1, -1, -7, -1, -1],
[-1, -1, -7, -1, -1, -1, -1, -1],
[-1, -1, -1, -7, -1, -1, -1, -1],
[-1, -1, -1, -1, -7, -1, -1, -1],
[-7, -1, -7, -1, -1, -1, -1, -1],
[-1, -1, -1, -7, -1, -1, -1, -1],
[-1, -1, -1, -1, -7, -7, -1, -1],
[-1, -1, -7, -1, -1, -1, -1, -1],


CODE 변수 선언

  • -1, -7과 같이 숫자로만 표기하면 숫자가 뭘 의미하는지 모르므로, 코드 변수들을 저장한 객체를 선언한다.

  • export 하여 모듈로 사용 가능하도록 한다.

export const CODE = {
  MINE: -7,
  NORMAL: -1,
  QUESTION: -2,
  FLAG: -3,
  QUESTION_MINE: -4,
  FLAG_MINE: -5,
  CLICKED_MINE: -6,
  OPENED: 0, 
};
* * *

plantMine 함수 로직

const plantMine = (row, cell, mine) => {
  console.log(row, cell, mine);

  const candidate = Array(row * cell)
    .fill()
    .map((arr, i) => i);
  const shuffle = [];
  while (candidate.length > row * cell - mine) {
    shuffle.push(
      candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
    );
  }
  const data = [];
  for (let i = 0; i < row; i++) {
    const rowData = [];
    data.push(rowData);
    for (let j = 0; j < cell; j++) {
      const cellData = [];
      rowData.push(CODE.NORMAL);
    }
  }
  for (let k = 0; k < shuffle.length; k++) {
    const ver = Math.floor(shuffle[k] / cell);
    const hor = shuffle[k] % cell;

    data[ver][hor] = CODE.MINE;
  }
  console.log(data);
  return data;
};

1. candidate 배열 만들기

  • row * cell 만큼의 칸 만들기.
  • 만약 10*10이라면? 0~99까지를 갖는 배열 만들기.
    (map으로 index를 리턴함)

2. shuffle

  • candidate 배열에서 mine 만큼 랜덤으로 뽑기.
    -> 0~99까지의 숫자 중 20개가 뽑히게 됨.
  • 랜덤으로 뽑기 위해 빈 배열인 shuffle에
    push 하는 방식으로.
  • candidate 배열을 splice하여 잘라냄.

3. NORMAL 코드 채움

  • 중첩 반복문을 통해 row~cell을 순회하며
    1) data에 rowData (빈 배열)을 push한 후,
    2) rowData에 CODE.NORMAL을 push함.

1) data = []
2) data.push(rowData)
-> data = [[],[],[],[],[],[],[],[],[],[]]
3) rowData.push(CODE.NORMAL)
-> data = [[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
... ]

-> 10*10의 경우 모든 100개의 요소들이 CODE.NORMAL(-1)이 된 것임.


4. MINE 심기

  • shuffle 배열 length만큼 순회하는 반복문
  • 이차원 배열이므로 행, 열을 구해서
    (ver, hor) 위치에 맞게 CODE.MINE을 대입함.

✅ 참고 - 이차원배열 위치 구하기

  • array [number / cell][number % cell]

console.log(data)를 해보면
아래와 같이 총 10*10 테이블에 20개의 지뢰가 심어져있다.


useContext로 렌더링

  • 이제 지뢰 칸을 각각 렌더링 해주면 된다.
  1. Table.jsx
import React, { useContext } from 'react';
import Tr from './Tr';
import { TableContext } from './MineSearch';

const Table = () => {
  const { tableData } = useContext(TableContext);
  return (
    <table>
      {Array(tableData.length)
        .fill()
        .map((tr, i) => (
          <Tr rowIndex={i} />
        ))}
    </table>
  );
};
export default Table;
  • useContext와 TableContext 임포트.
  • 지난 예제에서처럼 Array(length).fill().map()을 이용하여
    tableData의 요소들을 렌더링함.
  • 10*10일때, 2차원 배열인 tableData의 length=10이다.
  • 총 10개의 Tr이 렌더링됨.

  1. Tr.jsx
import React, { useContext } from 'react';
import Td from './Td';
import { TableContext } from './MineSearch';

const Tr = ({ rowIndex }) => {
  const { tableData } = useContext(TableContext);
  return (
    <tr>
      {tableData[0] &&
        Array(tableData[0].length)
          .fill()
          .map((td, i) => <Td rowIndex={rowIndex} cellIndex={i} />)}
    </tr>
  );
};
export default Tr;
  • 마찬가지로 useContext와 tableContext 임포트.
  • 여기서는 한 row에 해당하는 셀의 length만큼 반복.
    -> tableData[0]은 [-1,-1,-1,-1,-1,-1,-7,-1,-1,-1]과 같이 총 10개의 요소가 존재함.
  • 따라서 10개의 Td가 렌더링됨


Td 컴포넌트 스타일링

  • 스타일과 텍스트를 입히기.
  • 함수를 사용. (getTdStyle, getTdText)

Td.jsx

import React, { useContext } from 'react';
import { CODE, TableContext } from './MineSearch';

const getTdStyle = (code) => {
  switch (code) {
    case CODE.NORMAL:
    case CODE.MINE:
      return {
        background: '#444',
      };
    case CODE.OPENED:
      return {
        background: '#fff',
      };
    default:
      return {
        background: '#fff',
      };
  }
};

const getTdText = (code) => {
  switch (code) {
    case CODE.NORMAL:
      return '';
    case CODE.MINE:
      return 'X';
    default:
      return '';
  }
};

const Td = ({ rowIndex, cellIndex }) => {
  const { tableData } = useContext(TableContext);
  return (
    <td style={getTdStyle(tableData[rowIndex][cellIndex])}>
      {getTdText(tableData[rowIndex][cellIndex])}
    </td>
  );
};
export default Td;
  • JSX의 style 어트리뷰트에 함수의 리턴값을 넣어줄 수 있다.
  • 함수의 인자로 tableData배열의 요소를 받는다.
    -1 (CODE.NORMAL), -7(CODE.MINE) 등을 구분할 수 있다.
  • text의 경우에는 지뢰는 x로 표시되게 함.
  • background 색을 시커멓게 하고,
    클릭하면 하얗게 되도록 함.

<결과>

좋은 웹페이지 즐겨찾기